Patch Current WIM Files

When we started offering up patch current WIM files last year, we were trying to eliminate the time needed for patching during a deployment.  In beginning this process, I quickly wrote out a script to do what I wanted, and then put it away until the next month.  Each month it became a little more refined and easier to use.  Now I can kick off a couple VMs, run the script and a few hours later (depending on your anti-virus) have patch current WIMs the day after Patch Tuesday.

I have read many blogs on the subject and while they may not have had an all-in-one answer for me, I have taken pieces from everyone for what I was trying to do.  If I am using something you wrote, thank you.  I’ll have some acknowledgements at the end of the article.

Due to having to use a proxy, I cannot automate the gathering of the updates, but if you can, you can easily add that to this process.

First, we have a network share that keeps everything.  Different directories for OEM files, monthly patches, Upgrade packages, and reference WIMs.

 

Once 1803 came out, I saw that I needed to make this more dynamic for future releases.  To accommodate this I have a couple of questions based on which version you are editing, such as 1709, 1809, etc.  We also maintain different languages, so you would enter en-us or another language if needed.

#######################

Param(

[Parameter(Position=0, HelpMessage=”Enable pause for troubleshooting. Default is False”)] [ValidateSet(‘True’,’False’)] [string]$EnablePause = “False”,

[Parameter(HelpMessage=”Override destination source directory. Default is \\server.com\source_directory”)] [string]$DestDirectory = “\\server.com\source_directory”

)
[System.Reflection.Assembly]::LoadWithPartialName(‘Microsoft.VisualBasic’) | Out-Null
$OSVersion = [Microsoft.VisualBasic.Interaction]::InputBox(“Enter Windows 10 version `n (Example 1809 = 17763) `n 1709 = 16299 `n 1803 = 17134 `n 1809 = 17763”, “Windows 10 Version”, “16299”)
if($OSVersion.Length -lt 1)
{
Write-Host “No OS Value Entered”
Exit
}
Write-Host “$OSVersion” Selected -ForegroundColor Yellow

[System.Reflection.Assembly]::LoadWithPartialName(‘Microsoft.VisualBasic’) | Out-Null
$Language = [Microsoft.VisualBasic.Interaction]::InputBox(“Enter Language version `n (Example en-us) `n en-us “, “Language Version”, “en-us”)
if($Language.Length -lt 1)
{

Write-Host “No Language Value Entered”
Exit
}
Write-Host “$Language Selected” -ForegroundColor Yellow

[System.Reflection.Assembly]::LoadWithPartialName(‘Microsoft.VisualBasic’) | Out-Null
$CreateProdMedia = [Microsoft.VisualBasic.Interaction]::InputBox(“Create Production Media? `n (Example false) `n true `n or `n false”, “Create Production Media”, “false”)
if($CreateProdMedia.Length -lt 1)
{

Write-Host “No Production Media Value Entered”
Exit
}

#######################

The Create Production Media section above will create an SCCM Operating System Upgrade package for you if you select “true” when prompted.

Next, it is important to place your network shares correctly.  The naming convention I use for $0Source for the OEM WIM makes sense to me, but may not make sense to you, so update accordingly.

#########

Function DownloadSource {

Write-Host “Copying WIM File” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan
$copysourcedir = “\\server.com\source_directory”
$0Source = $OSVersion + “.0source_” + $Language
$OEM = (Get-ChildItem $copysourcedir -Directory | select-string -Pattern ($0source) | Sort-Object -Descending | Select-Object -First 1)
Write-Host “Copying $Language source files for Windows 10 $OSVersion” -ForegroundColor Yellow
copy-item $copysourcedir\$OEM\sources\install.wim $PSScriptRoot\$OSVersion\$Language\install_$Language.wim -Verbose

$TimeStamp = Get-Date
}

##########################

That will copy the WIM file to where you are running the script from.  It will create directories based on built number and language.

The next function is just a cleanup if the directories already exist on your system from a previous month.

##############

Function Cleanup {
Try {
Write-Host “Cleaning Up” -ForegroundColor Green
if (Test-Path -path $PSScriptRoot\$OSVersion\$Language) {Remove-Item -Path $PSScriptRoot\$OSVersion\$Language -Force -ErrorAction Continue}
& DISM /Cleanup-WIM
}
Catch
{
Write-Warning “Cleanup failed”
Throw $Error
}
}

################

After the functions are setup, we next start logging and check on a few items.  If ADK is not installed, it will exit out.  If the mount directory does not exist, it will create it.

 

########

#Script Logging. Stop Transcript if already running and begin.
$ErrorActionPreference=”SilentlyContinue”
Stop-Transcript | out-null
$ErrorActionPreference = “Continue” # or “Stop”
Start-Transcript -path “$PSScriptRoot\$OSVersion $Language updateWIM.log” -append

#Check for Windows ADK Installation
Write-Host “Checking for ADK Installation” -ForegroundColor Yellow
$ADKdir = Get-ChildItem “C:\Program Files (x86)\Windows Kits\10”
if (!( $ADKdir))
{Write-Host “Windows ADK is not installed.” -ForegroundColor DarkRed
Exit
}

#Check for Mount Directory
$Mountdir = “$PSScriptRoot\$OSVersion\$Language\mount”
if (!(test-path -path “$PSScriptRoot\$OSVersion\$Language\mount”)) {New-Item -ItemType directory -Path “$PSScriptRoot\$OSVersion\$Language\mount”
Write-Host “Creating Mount Directory” -ForegroundColor Yellow}
else
{Write-Host “Mount Directory Already Exists” -ForegroundColor Yellow}

########################

The next section will copy the updates that are being used to update the WIM, including any CU (I put flash and .net in the same folder) SSU or Dynamic Updates. It will look for the newest folder for the CU patches or the naming convention I have set up below.

#############################

#Copying Servicing Stack Update and Cumulative Updates
Write-Host “Copying Servicing Stack Update to Working Directory” -ForegroundColor Yellow
$PatchSource = “\\server.com\source_directory\Patches”
$SS = “SS”
$CU = “CU”
$DU = “DU”
$UPDATES = $SS,$CU,$DU
Foreach ($i in $UPDATES)
{
if (!(test-path -path “$PSScriptRoot\$OSVersion\$Language\$i”)) {New-Item -ItemType directory -Path “$PSScriptRoot\$OSVersion\$Language\$i”
Write-Host “Creating $i Directory” -ForegroundColor Yellow}
}
$SERVESTACK = “servicestack_” + $OSVersion
$PatchDir = (Get-ChildItem $PatchSource -Directory | select-string -Pattern ($SERVESTACK) | Sort-Object -Descending | Select-Object -First 1)
Write-Host “Copying $PatchDir” -ForegroundColor Yellow
copy-item $PatchSource\$PatchDir\* $PSScriptRoot\$OSVersion\$Language\SS #-Verbose

$PATCHFOLDER = $OSVersion + “.”
$PatchDir = (Get-ChildItem $PatchSource -Directory | select-string -Pattern ($PATCHFOLDER) | Sort-Object -Descending | Select-Object -First 1)
Write-Host “Copying $PatchDir” -ForegroundColor Yellow
copy-item $PatchSource\$PatchDir\* $PSScriptRoot\$OSVersion\$Language\CU #-Verbose

$DYNAMICUP = “DynamicUpdate_” + $OSVersion
$PatchDir = (Get-ChildItem $PatchSource -Directory | select-string -Pattern ($DYNAMICUP) | Sort-Object -Descending | Select-Object -First 1)
Write-Host “Copying $PatchDir” -ForegroundColor Yellow
copy-item $PatchSource\$PatchDir\* $PSScriptRoot\$OSVersion\$Language\DU -Recurse #-Verbose

###########################

Now we call the download function from above and download the OEM WIM file.  We then compare the WIM version versus the ADK installed in order to use the correct or newer version to modify the WIM file.

#################

#Download Source files
DownloadSource
Write-Host “Getting WIM and ADK information” -ForegroundColor Yellow
$WIMVersion = (Get-WindowsImage -ImagePath “$PSScriptRoot\$OSVersion\$Language\install_$Language.wim” -Name “Windows 10 Enterprise”).Version
$ENTindex = (Get-WindowsImage -ImagePath “$PSScriptRoot\$OSVersion\$Language\install_$Language.wim” -Name “Windows 10 Enterprise”).ImageIndex
$tempdir = Get-Location
$tempdir = $tempdir.tostring()
$dismpath = “C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\x86\DISM”
$appToMatch = ‘*Windows Assessment and Deployment Kit*’
$appVersion = $WIMVersion
$TimeStamp = Get-Date

$result = (get-item $dismpath\dism.exe).versioninfo.productversion

If ($result -lt $appVersion) {
Write-Host “Windows ADK is not correct version to modify WIM.” -ForegroundColor DarkRed
Exit
}
else
{Write-Host “Windows ADK $result and WIM Version $WIMVersion” -ForegroundColor Yellow

#################################

NOTE: If copying each section instead of downloading the script, that there is an open bracket at the Write-Host that continues below.

We now extract the WIM.

####################

# Extract ENT WIM
Write-Host “Exporting Enterprise WIM to new file” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan
Dism /Export-Image /SourceImageFile:”$PSScriptRoot\$OSVersion\$Language\install_$Language.wim” /SourceIndex:$ENTindex /DestinationImageFile:”$PSScriptRoot\$OSVersion\$Language\install-optimized_$Language.wim”
Write-Host “Export Index $ENTindex Completed” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan

##################

Next we just mount the WIM.  Timestamps if you want to see how long you were bored.

###################

# Mount Wim File
Write-Host “Mounting WIM File” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan
#$ENTindex = (Get-WindowsImage -ImagePath “$PSScriptRoot\$OSVersion\$Language\install_optimized_$Language.wim” -Name “Windows 10 Enterprise”).ImageIndex
dism /mount-wim /wimfile:$PSScriptRoot\$OSVersion\$Language\install-optimized_$Language.wim /index:1 /mountdir:$PSScriptRoot\$OSVersion\$Language\mount
Write-Host “Mounting Completed” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan

###################

Now we simply patch the mounted WIM with the Servicing Stack, CU, and any Dynamic Updates.

###############

Write-Host “Updating WIM” -ForegroundColor Yellow
Foreach ($i in $UPDATES)
{
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan
dism /image:$PSScriptRoot\$OSVersion\$Language\mount /add-package /packagepath:$PSScriptRoot\$OSVersion\$Language\$i
Write-Host “$i Updates Completed” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan
}

If ($EnablePause -eq “True”) {
pause
}

####################

This is where the pause is in the script if enabled True.  This allows you to check the for whatever reason you may have.

Next we reset the base and cleanup the WIM.

#############

# Cleanup WIM
Write-Host “Cleaning up WIM” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan
DISM /Cleanup-Image /Image=$PSScriptRoot\$OSVersion\$Language\mount /StartComponentCleanup /ResetBase
Write-Host “Cleanup Completed” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan

##############

And simply unmount the WIM.

####################

#Unmount WIM
Write-Host “Unmounting WIM File” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan
Dism /Unmount-Image /MountDir:”$PSScriptRoot\$OSVersion\$Language\mount” /Commit
Write-Host “Unmount Completed” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan

#####################

Now, if you said “True” for create production media it will first update your MDT with the WIM file.  (If you do not create reference images, you can eliminate where needed.)

########################

If ($CreateProdMedia -eq “True”) {
If ($Language -eq “en-us”) {
#Copy to Production MDT
Write-Host “Copy WIM file to Production MDT” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan
$MDTwimpath = “\\MDTserver\Win10x64\Operating Systems”
$MDTOSVAR = “_b” + $OSVersion
$MDTsourceDir = (Get-ChildItem $MDTwimpath -Directory | select-string -Pattern ($MDTOSVAR) | Sort-Object -Descending | Select-Object -First 1)
copy-item $PSScriptRoot\$OSVersion\$Language\install-optimized_$Language.wim $MDTwimpath\$MDTsourceDir\Sources\install.wim -Force
Write-Host “Copy To Production MDT Completed” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan } #Copy to MDT close bracket

####################

The next section will create your upgrade package files that will be ready to be used with SCCM in a brand new directory and include any Dynamic Updates.

###################

#Create Upgrade Files
Write-Host “Creating Upgrade Files” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan
$PatchDir = (Get-ChildItem $PatchSource -Directory | select-string -Pattern ($PATCHFOLDER) | Sort-Object -Descending | Select-Object -First 1)
$UpdateVER = (“$PatchDir”).Remove(0,14)
Write-Host “$UpdateVER” -ForegroundColor Yellow
$DestDirectory = “\\server\destination”
$OS =”W10x64.ENT_” + $OSVersion + “_”
$NewDirectory = $OS + “.” + $UpdateVER + “_” + $Language
$0Source = $OSVersion + “.0source_” + $Language
$OEM = (Get-ChildItem $DestDirectory -Directory | select-string -Pattern ($0source) | Sort-Object -Descending | Select-Object -First 1)
$NewDirectoryName = (“$OEM”).Remove(17,19) + $UpdateVER + “_” + $Language
Write-Host “New Directory Name is $NewDirectoryName” -ForegroundColor Yellow
New-Item -ItemType directory -Path $DestDirectory\$NewDirectoryName
Copy-Item “$DestDirectory\$OEM\*” -Destination “$DestDirectory\$NewDirectoryName\” -Recurse -Verbose
Write-Host “OEM Source Files Copied” -ForegroundColor Yellow

$DYNAMICUP = “DynamicUpdate_” + $OSVersion
$PatchDir = (Get-ChildItem $PatchSource -Directory | select-string -Pattern ($DYNAMICUP) | Sort-Object -Descending | Select-Object -First 1)
copy-item $PatchSource\$PatchDir\sources* -Destination “$DestDirectory\$NewDirectoryName\Sources\” -Recurse -Verbose
Write-Host “Dynamic Update Source Files Copied… Now Copying Updated WIM File.” -ForegroundColor Yellow

copy-item $PSScriptRoot\$OSVersion\$Language\install-optimized_$Language.wim $DestDirectory\$NewDirectoryName\Sources\install.wim -Force
Write-Host “UIP Files Completed” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan

######################

SCCM Admin Console should be installed in order to run the next section.  Your site code and provider machine name need to be entered.

After the standard generated code, a package is created in the Admin Console with current month information and build number.

#################

###### Generated from Configuration Manager
#
# Press ‘F5’ to run this script. Running this script will load the ConfigurationManager
# module for Windows PowerShell and will connect to the site.
#
# This script was auto-generated at ‘5/30/2018 8:08:22 AM’.

# Uncomment the line below if running in an environment where script signing is
# required.
#Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process

# Site configuration
$SiteCode = “XXX” # Site code
$ProviderMachineName = “server.name.com” # SMS Provider machine name

# Customizations
$initParams = @{}
#$initParams.Add(“Verbose”, $true) # Uncomment this line to enable verbose logging
#$initParams.Add(“ErrorAction”, “Stop”) # Uncomment this line to stop the script on any errors

# Do not change anything below this line

# Import the ConfigurationManager.psd1 module
if((Get-Module ConfigurationManager) -eq $null) {
Import-Module “$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1” @initParams
}

# Connect to the site’s drive if it is not already present
if((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) {
New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $ProviderMachineName @initParams
}

# Set the current location to be the site code.
Set-Location “$($SiteCode):\” @initParams

####### End of Generated Configuration Manager Code
$SCCMVersion = “10.0.” + $UpdateVER
$SCCMName = (“$NewDirectoryName”).Remove(0,11)
$SCCMName2 = (“$SCCMName”).split(‘_’)[0] $SCCMName3 = “Win10x64 ” + $SCCMName2 + ” ” + $Language + ” ” + “($UpdateVER)”
$SCCMdescription = get-date -UFormat “%B %Y”

########################

This is the section is the actual detail for the packages created.  It will also update your task sequence (we use test task sequences.)

####################

if ($OSVersion -eq 16299) {
$RETIRED = (Get-CMOperatingSystemInstaller -Name “RETIRED_*1709*$Language”) $RETIRED_ID = ($RETIRED.packageID)
Set-CMOperatingSystemInstaller -Id $RETIRED_ID -NewName “NEW_1709 _$Language” -Description “NEW” -Path “$DestDirectory\$NewDirectoryName” -Version “$SCCMVersion”
$NEWNAME = (Get-CMOperatingSystemInstaller -Name “NEW_*1709*$Language”)
if ($Language -eq en-us) {
Set-CMTSStepUpgradeOperatingSystem -TaskSequenceId XXXXXXXX -UpgradePackage $NEWNAME }
}

if ($OSVersion -eq 17134) {
$RETIRED = (Get-CMOperatingSystemInstaller -Name “RETIRED_*1803*$Language”) $RETIRED_ID = ($RETIRED.packageID)
Set-CMOperatingSystemInstaller -Id $RETIRED_ID -NewName “NEW_1803 _$Language” -Description “NEW” -Path “$DestDirectory\$NewDirectoryName” -Version “$SCCMVersion”
$NEWNAME = (Get-CMOperatingSystemInstaller -Name “NEW_*1803*$Language”)
if ($Language -eq en-us) {
Set-CMTSStepUpgradeOperatingSystem -TaskSequenceId XXXXXXXX -UpgradePackage $NEWNAME }

}

############################

Write-Host “Configuration Manger Package Creation Complete” -ForegroundColor Yellow
$TimeStamp = Get-Date
Write-Host “$TimeStamp” -ForegroundColor Cyan} #”Configuration Manager CMDLETS found Close Block
}
} #Process Close Block

Cleanup

Stop-Transcript

#############################

And that’s it.  I had to clean it up a bit in order to share, but those are the main steps and processes.  I will probably come back and add to it as I add more items.  Including replicating out to a DP group as part of the process.  I have hesitated as our SCCM guys don’t like when we replicate WIMS without checking site replication backlog first.

Feel free to contact me and comment or suggest as you want.

https://github.com/shotgn22/osd   (wimUpdate.ps1)

Thank you for reading this far.

My name is Scott Graves, I have been doing IT since 1995 and am currently heavy into OSD, Task Sequences and Upgrades.  I have set up a Twitter (@shotgn22) if you have any questions.  Thank you Adam Gross of ASquareDozen.com and Chris Buck of this page. Dave Segura of OSD Builder, Mike Terrill, Gary Block, Michael Neuhaus and many others are sites I visit regularly.  Without them, I wouldn’t have been able to automate what  I have thus far.  That said, this works great for my situation, and I know everyone has different environments and situations, but I hope that if anything, there is something I have used above that makes the “ding” go off and helps someone out.

Performance Counter Script

, ,

This particular Counter script started out as a project tasking.   The request was simple, if not vague: find out why our computers are so slow.  Rather than remoting into a bunch of random computers and then taking screen shots of Task Manager, I came up with the following script: Download

I’ll walk through some basic examples of what the parameters do, and then delve into how they do it.

Example 1:

.\LocalCounterScript.ps1 -TargetComputer TEST-BOX-1 -MonitorDuration 5 -SamplingRate 5

This example runs the Counter script against a remote system, it runs for five minutes, and it takes one sample every 5 seconds.  We’re not looking for any specific processes here, so it’s going to capture from the master list embedded in the script itself.   When it finishes running, the script will not only dump a CSV report containing the highest (or lowest where relevant) values, but it will also dump the raw data it collected.

Example 2:

.\LocalCounterScript.ps1 -TargetComputer TEST-BOX-2 -TargetProcess TaniumClient -logPath C:\Temp

This time, we’re still going into a remote system, but we’re specifically looking for the TaniumClient process.  While we are only capturing that one application process, the script will always capture basic system performance counters such as hard drive activity, memory usage, and CPU usage.  Also, we’ve redirected the log file to a new location.  Because we didn’t specify a monitor duration or frequency, it falls back to the script defaults of one minute duration with counters captured every two seconds.

Alternatively, you can run this script against a list of systems via a simple loop. Combined with the start-job cmdlet, you can quickly collect performance data from a wide range of systems.

 

But How Does It Work

Starting with the parameter block, here are the basic items I’ve decided we would want to change:

param (
        [Parameter()]
        [string]$TargetComputer=$ENV:COMPUTERNAME,
        [Parameter()]
        [double]$monitorDuration = 1,
        [Parameter()]
        [int]$samplingRate = 2,
        [Parameter()]
        [string]$logPath = "\\testServer\uploads\synack\counterscript\",
        [Parameter()]
        [string]$targetProcess = "BLANK"  # I put this in here if you want to only track a specific process (i.e collecting Tanium Report data so you only want to monitor the TaniumClient process). It also collects the generic system ones
)

The first thing we might want to change is the target computer. Since I was frequently running this script against a computer I was already logged into, the default value is left as the local computer.  While you can run this with “hostname” or even nothing at all, but because this generates a report at the end, having the actual computer name matters.

Monitor duration and sampling rate are pretty much what they sound like: how long do you want to monitor the computer and how frequently are you checking your counters.

Log Path is similarly self explanatory.  This is a root folder more than a specific name since if you’re doing multiple computers at a time, I didn’t want to specify a log file name.

Target Process is where you specify what process you’re trying to monitor. If you don’t specify anything, it will monitor everything on the master list I’ve hard coded into the script.  This was based on a list of all the processes we thought would significantly impact system performance.  If you do specify a process, it monitors that process as well as some basic parameters like hard drive, memory, and CPU usage.

# Variable Declaration
$masterCountersList = @() #this is the array that will hold the GenericCounters combined with the process specific counters so I can run them all at once
$startTime = (Get-Date -format "yyyy-MM-d_hhmmss")
$GenericCounters =
"\\$TargetComputer\PhysicalDisk(*)\% Idle Time",
"\\$TargetComputer\Memory\% committed bytes in use",
"\\$TargetComputer\Memory\Available MBytes",
"\\$TargetComputer\Memory\Free System Page Table Entries",
"\\$TargetComputer\Memory\Pool Paged Bytes",
"\\$TargetComputer\Memory\Pool Nonpaged Bytes",
"\\$TargetComputer\Memory\Pages/sec",
"\\$TargetComputer\Processor(_total)\% processor time",
"\\$TargetComputer\Processor(_total)\% user time" # these are all the counters that aren't process specific

This section is all of the basic counters I want to track whether I’m after a specific process or not.  Also, this sets our $startTime variable, which we’re using for logging purposes.

After the generic counters comes the massive list of counters we were tracking on a generic system scan.  I’m not going to paste them here, but they include everything from antivirus to Tanium client and SCCM.

# This turns the process names into full counter paths so we don't have to enter three of them per each process we add later down the line
function Return-CounterArray ($processName)
{
      $counters = @()
      $counters += "\\$TargetComputer\Process($processName)\Handle Count"
      $counters += "\\$TargetComputer\Process($processName)\Thread Count"
      $counters += "\\$TargetComputer\Process($processName)\Private Bytes"
      $counters += "\\$TargetComputer\Process($processName)\% Processor Time"
      return $counters
}

# This takes the generic counters and adds them to the master list along with all the processs based ones. I feel like this could probably be rolled up into the Return-CounterArray one, but it looks pretty like this
function Generate-Counters (){
    $allCounters = @()
    $allCounters += $GenericCounters
    $ProcessList | % {$allCounters += (Return-CounterArray $_)}
    return $allCounters
}

This section actually takes all of those processes in the list and generates the four different counters we want to track for each one. If I don’t use a function like this, we just end up with a colossal list in the script itself, and nobody wants that.

# Actually start the script now
$ReportArray = @() # this holds all the generate report objects for later export
$TempArray = @() # this holds the objects while we figure out the highest value
Write-host -ForegroundColor Green "$(get-date -format hh:mm:ss) - Starting report on $TargetComputer. Please be patient as the process begins."
Write-Host -ForegroundColor Green "$(get-date -format hh:mm:ss) - Generating master list based on $($GenericCounters.Count) System counters and $($ProcessList.count) Processes."
$masterCountersList = (Generate-Counters)
Write-Host -ForegroundColor Green "$(get-date -format hh:mm:ss) - Master list created with $($masterCountersList.count) items."
$maxSamples = [Math]::Round(($monitorDuration*60/$samplingRate), 0)	 #multiplies your monitor duration minutes by 60 and divides by your sampling interval. Rounds to 0 decimal places because Integers
Write-Host -ForegroundColor Green "$(get-date -format hh:mm:ss) - Will take $maxSamples samples over the course of $monitorDuration minutes."
$rawCounterDump = @()

# This actually goes and gets the counter information. Woot. 
$rawCounterDump = Get-Counter -Counter $masterCountersList -SampleInterval $samplingRate -MaxSamples $maxSamples -ComputerName $TargetComputer -ErrorAction SilentlyContinue

# This will export everything to BLG files so you can review them in Perfmon later if you'd like (gives a pretty line graph!) 
if ($logPath[-1] -ne "\") {$logPath += "\"}
$endTime = (Get-Date -format "yyyy-MM-d_hhmmss")
$blgDump = $logPath+"$TargetComputer-$endTime-RawData.blg"
Write-Host -ForegroundColor Green "$(get-date -format hh:mm:ss) - Dumping raw Perfmon data to $blgDump."

Now we’re getting into what actually does the work.  We’re using our get-date cmdlet to track time along the way, just in case something hangs. We’re also declaring our different arrays for holding information.  I also have it give me a count of the counters and processes, mostly for diagnostic purposes. If something looks off, it probably is.  This is also where we do a bit of math to tell the Get-Counter cmdlet how many samples we’re taking. Since it only handles integers, we have to round it to 0 decimal places.  After the math and setup is complete, we get the counters from our target computer, log when we finish, and dump the raw data as a BLG file that you can open later in PerfMon.

# now that the raw data has already been exported, this chunk turns that raw data into an array for further processing.
$rawCounterDump | Export-Counter -Path $blgDump
$rawCounterDump.countersamples | % {
    $path = $_.Path
    $obj = new-object psobject -property @{
        ComputerName = $TargetComputer
        Counter = $path.Replace("\\$($TargetComputer.ToLower())","")
        Item = $_.InstanceName
        Value = [Math]::Round($_.CookedValue, 2)	
        DateTime = (Get-Date -format "yyyy-MM-d hh:mm:ss")
    }
    $TempArray += $obj
}
Write-Host -ForegroundColor Green "$(get-date -format hh:mm:ss) - $($TempArray.count) total samples collected."

Here, we are taking that raw data and converting it to an object array that is easier to search later.   Again, this outputs some diagnostic information just in case something looks off during the conversion.

# This bit takes all the entries in TempArray, gets the unique counter names, finds all entries for that counter name, looks for the highest (or lowest where it matters) value, and then adds only the matching entry to the "highest value" report
$UniqueCounters = ($TempArray | select -Property Counter -Unique).counter
Write-Host -ForegroundColor Green "$(get-date -format hh:mm:ss) - $($UniqueCounters.count) unique counters discovered"
foreach ($c in $UniqueCounters)
{
    $targetEntries = $TempArray | ? {$_.Counter -eq $c}
    if ($c -eq "\PhysicalDisk(*)\% Idle Time" -or $c -eq "\Memory\Available MBytes" -or $c -eq "\Memory\Pool Nonpaged Bytes") {$highValue = ($targetEntries | Measure-Object -Property Value -Minimum).Minimum}
    else {$highValue = ($targetEntries | Measure-Object -Property Value -Maximum).Maximum}
    $selectedEntry = $TempArray | ? {$_.Counter -eq $c -and  $_.Value -eq $highValue}
    if ($selectedEntry.count -gt 1) {$selectedEntry = $selectedEntry[0]}
    $ReportArray += $selectedEntry
}

In this specific case, we wanted the most “significant” value for each counter over the measured time period. For available memory, that would be the lowest number, for a process CPU usage counter, it would be the highest number.   We find each uniquely named counter, look for everything that has that name, and then find the most significant value for that name.  Once we have it, we save it to our reporting array.

# Generates a file name based on what you asked the script to do, and dumps it to a CSV for manager-ization later. 
if ($targetProcess -eq "BLANK") {$outLog = $logPath+"$TargetComputer-$startTime-to-$endTime-Results.csv"}
else {$outLog = $logPath+"$TargetComputer-$TargetProcess-$startTime-to-$endTime-Results.csv"}
Write-Host -ForegroundColor Green "$(get-date -format hh:mm:ss) - Writing report to $outLog."
$ReportArray | Export-Csv -Path $outLog -NoClobber -NoTypeInformation -Force
Write-Host -ForegroundColor Green "$(get-date -format hh:mm:ss) - Complete.`n"

This generates the log file based on the parameters you provided earlier. If you were monitoring a specific process, this will name the log file based on that.  Otherwise, it just uses the computer name. The date and time are stamped on as well for future reference, and a spreadsheet is generated for later processing by managers who like spreadsheets.   If your IT department is like ours, you probably already know which processes are killing your CPU cycles, but this will let your manager know that you’ve done due diligence.  I hope this helps, and it sure beats taking screen captures of Perfmon for hours on end.

 

Below is an example of what we found on a test system. From the screenshot we determined that Tanium was no Bueno.

Counter Script

 

ALSO CHECK : We should talk about incremental collection updates…