Automation in Patch Managment…
One of my areas of my work has been to handle Patch Managment for x-numbers of clients and with multiple clients everything becomes more complicated to handle.
Therefore i’ve started to try to automate as much as possible with everything about Patch Managment by using Powershell.
What I have collected in this article will help you set up Maintenance Windows, send emails with a list of downloaded and published updates, and emails about status after the updates are completed.
You can choose to go through the article and copy the scripts manually, or you can go to my github and download all the scripts at once.
Link to my Githb where you find all scripts
Before you start…
You need to have the following already configured in your enviroment.
- X-numbers of Collections with devices ready for patches
- A plan when you want to have your Maintenance Windows for each Collection
- ADR setup with Update Package and Update Group
- You need a standard domain user with read-rights in MECM and with rights to run batch job on server where you will run the schedule Tasks
Note: Your ADR must not create new Update Group for each month, it needs to reuse existing Update Group to make the email created in step two and three to work.
Step One – Maintenance Windows
The first you need to do, and do before every new year are to run a script to set Maintenance Windows on your Collections.
Script requirements
|
In the script i have a function called “Get-PatchTuesday” and this functions calculate which day in month “Patch Tuesday” occurs.
What data you need to run the script:
- Numbers of weeks after Patch Tuesday
- Numbers of days after Patch Tuesday
- Start hour
- Start minute
- Lenght i hours (0-23)
- Lenght in minutes (0-59)
- Delete old Maintenance Windows (Yes/No)
- Allowed in Maintenance Windows (Everything, Task Sequence or Updates only)
- Collection ID
- For which month you want your Maintenance Windows (1-12)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
<# .SYNOPSIS This script configures multiple Maintenance Windows for a collection.The schedule is based on offset-settings with Patch Tuesday as base. .DESCRIPTION This script give you options to delete existing Maintenance Windows on collection, decide if the Maintenance Windows should be for Any installation, Task sequence or only SoftwareUpdates. You can also decide which month the Maintenance Windows should be configured for. Some of the funcionality has been borrowed from Daniel Engberg´s script, created 2018 which he borrowed som functionality from Octavian Cordos' script, created in 2015. #################### Christian Damberg www.damberg.org Version 1.0 2021-12-22 #################### .EXAMPLE .\Set-Maintenance.ps1 -CollID ps100137 -OffSetWeeks 1 -OffSetDays 5 -AddStartHour 18 -AddStartMinutes 0 -AddEndHour 4 -AddEndMinutes 0 -PatchMonth "1","2","3","4","5","6","7","8","9","10","11" -patchyear 2022 -ClearOldMW Yes -ApplyTo SoftWareUpdatesOnly Will create a Maintenance Window with Patch Tuesday + 1 week and 5 days for collection with ID PS100137 for every month except december in 2022. The script also delete old Maintance Windows and the new Maintance Windows are only for SoftwareUpdates. .DISCLAIMER All scripts and other Powershell references are offered AS IS with no warranty. These script and functions are tested in my environment and it is recommended that you test these scripts in a test environment before using in your production environment. #> PARAM( [int]$OffSetWeeks, [int]$OffSetDays, [Parameter(Mandatory=$True)] [int]$AddStartHour, [Parameter(Mandatory=$True)] [int]$AddStartMinutes, [Parameter(Mandatory=$True)] [int]$AddEndHour, [Parameter(Mandatory=$True)] [int]$AddEndMinutes, [Parameter(Mandatory=$True)] [string[]]$PatchMonth, [Parameter(Mandatory=$True)] [int]$patchyear, [Parameter(Mandatory=$True)] [ValidateSet('Yes','No')] [string]$ClearOldMW = "No", [Parameter(Mandatory=$True)] [ValidateSet('SoftWareUpdatesOnly','TaskSequenceOnly','Any')] [string]$ApplyTo = "SoftWareUpdatesOnly", [Parameter(Mandatory=$True,Helpmessage="Provide the ID of the collection")] [string]$CollID ) #region Initialize #Load SCCM Powershell module Try { Import-Module (Join-Path $(Split-Path $env:SMS_ADMIN_UI_PATH) ConfigurationManager.psd1) -ErrorAction Stop } Catch [System.UnauthorizedAccessException] { Write-Warning -Message "Access denied" ; break } Catch [System.Exception] { Write-Warning "Unable to load the Configuration Manager Powershell module from $env:SMS_ADMIN_UI_PATH" ; break } #endregion #region functions #Set Patch Tuesday for a Month Function Get-PatchTuesday ($Month,$Year) { $FindNthDay=2 #Aka Second occurence $WeekDay='Tuesday' $todayM=($Month).ToString() $todayY=($Year).ToString() $StrtMonth=$todayM+'/1/'+$todayY [datetime]$StrtMonth=$todayM+'/1/'+$todayY while ($StrtMonth.DayofWeek -ine $WeekDay ) { $StrtMonth=$StrtMonth.AddDays(1) } $PatchDay=$StrtMonth.AddDays(7*($FindNthDay-1)) return $PatchDay Write-Log -Message "Patch Tuesday this month is $PatchDay" -Severity 1 -Component "Set Patch Tuesday" } #Remove all existing Maintenance Windows for a Collection Function Remove-MaintenanceWindow { PARAM( [string]$CollID ) $SiteCode = Get-PSDrive -PSProvider CMSITE Set-Location -Path "$($SiteCode.Name):\" $OldMW = Get-CMMaintenanceWindow -CollectionId $CollID #Get-CMMaintenanceWindow -CollectionId $CollID | Where-Object {$_.StartTime -lt (Get-Date)} | ForEach-Object { Get-CMMaintenanceWindow -CollectionId $CollID | ForEach-Object { Try { Remove-CMMaintenanceWindow -CollectionID $CollID -Name $_.Name -Force -ErrorAction Stop $Coll=Get-CMDeviceCollection -CollectionId $CollID -ErrorAction Stop Write-Log -Message "Removing $($_.Name) from collection $MWCollection" -Severity 1 -Component "Remove Maintenance Window" Write-Output "Removing $($_.Name) from collection $MWCollection" } Catch { Write-Log -Message "Unable to remove $($_.Name) from collection $MWCollection" -Severity 3 -Component "Remove Maintenance Window" Write-Warning "Unable to remove $($_.Name) from collection $MWCollection. Error: $_.Exception.Message" } } Set-Location $PSScriptRoot } #Function for append events to logfile located c:\windows\logs Function Write-Log { PARAM( [String]$Message, [int]$Severity, [string]$Component ) Set-Location $PSScriptRoot $Logpath = "C:\Windows\Logs" $TimeZoneBias = Get-CimInstance win32_timezone $Date= Get-Date -Format "HH:mm:ss.fff" $Date2= Get-Date -Format "MM-dd-yyyy" $Type=1 "<![LOG[$Message]LOG]!><time=$([char]34)$Date$($TimeZoneBias.bias)$([char]34) date=$([char]34)$date2$([char]34) component=$([char]34)$Component$([char]34) context=$([char]34)$([char]34) type=$([char]34)$Severity$([char]34) thread=$([char]34)$([char]34) file=$([char]34)$([char]34)>"| Out-File -FilePath "$Logpath\Set-MaintenanceWindows.log" -Append -NoClobber -Encoding default $SiteCode = Get-PSDrive -PSProvider CMSITE Set-Location -Path "$($SiteCode.Name):\" } #endregion #region Parameters $SiteCode = Get-PSDrive -PSProvider CMSITE Set-Location -Path "$($SiteCode.Name):\" $GetCollection = Get-CMDeviceCollection -ID $CollID $MWCollection = $GetCollection.Name $ErrorMessage = $_.Exception.Message #endregion #Delete old Maintance Windows if value in $ClearOldMW equals Yes if ($ClearOldMW -eq 'Yes') { Remove-MaintenanceWindow -CollID $CollID } #Create Maintance Windows for for every month specified in variable Patchmonth foreach ($Monthnumber in $PatchMonth) { $MonthArray = New-Object System.Globalization.DateTimeFormatInfo $MonthNames = $MonthArray.MonthNames #Set Patch Tuesday for each Month $PatchDay = Get-PatchTuesday $Monthnumber $PatchYear #Fix to get the right Name of Maintance Windows month. $displaymonth = $Monthnumber - 1 #Set Maintenance Window Naming Convention (Months array starting from 0 hence the -1) $NewMWName = "MW_"+$MonthNames[$displaymonth]+"_"+$patchyear $SiteCode = Get-PSDrive -PSProvider CMSITE Set-Location -Path "$($SiteCode.Name):\" #Set Device Collection Maintenace interval $StartTime=$PatchDay.AddDays($OffSetDays).AddHours($AddStartHour).AddMinutes($AddStartMinutes) $EndTime=$StartTime.Addhours(0).AddHours($AddEndHour).AddMinutes($AddEndMinutes) Try { #Create The Schedule Token $Schedule = New-CMSchedule -Nonrecurring -Start $StartTime.AddDays($OffSetWeeks*7) -End $EndTime.AddDays($OffSetWeeks*7) -ErrorAction Stop New-CMMaintenanceWindow -CollectionID $CollID -Schedule $Schedule -Name $NewMWName -ApplyTo $ApplyTo -ErrorAction Stop Write-Log -Message "Created Maintenance Window $NewMWName for Collection $MWCollection" -Severity 1 -Component "New Maintenance Window" #Write-Output "Created Maintenance Window $NewMWName for Collection $MWCollection" } Catch { Write-Warning "$_.Exception.Message" Write-Log -Message "$_.Exception.Message" -Severity 3 -Component "Create new Maintenance Window" } } Set-Location $PSScriptRoot<span id="ib-toc-anchor-3"></span> |
Step Two – Send-UpdateDeployedMail.ps1
This script generate and send a Email where the recipient get information of downloaded and published Microsoft Updates in a specified Update Group with hyperlinks to kb-article.
This script should be executed x-days after Patch Tuesday and before Maintenance Windows goes active.
To be able to run UpdateDeployedMail.ps1 you need to install Powershell 7.x. The script uses a module called “Send-MailkitMessage”. You also need MECM-Console installed or execute the script on the site-server.
The server where you execute the script need to be white-listed in your Exchange.
Things you need to configure in the script…
- LimitDays – How many days back in time you want the script to check for updates
- Sitecode – Your sitecode
- UpdateGroupName – Your Update Group
- EmailFrom – Your no-reply address
- Email_Error – If hte script can´t find any Updates downloaded or Published according to your LimitDays it will send a Email to a group och user to notify of a problem.
- Email_Success – The group or user to recieve Email
- SMTP – Your SMTP-server
- Portnumber – Your portnumber for your SMTP-server (default 25)
- Customer – The Company name
Your also need to customize the Email and for that you need some html-knowledge (You can find the most on Google 🙂 )
Script requirements
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 |
######################################################### #.Synopsis # List all updates member of updategroup in x-numbers of days #.DESCRIPTION # Lists all assigned software updates in a configuration manager 2012 software update group that is selected # from the list of available update groups or provided as a command line option #.EXAMPLE # Send-UpdateDeployedMail.ps1 # # Skript created by Christian Damberg # christian@damberg.org # https://www.damberg.org # ######################################################### # Values need in script ######################################################### # # Numbers of days backwards you want to check for updates in updategroup $LimitDays = '-15' $SiteCode = '<sitecode>' $UpdateGroupName = '<the name of your update group>' $MailFrom = '<Your no-reply-address>' $Mail_Error = '<Your mail to send when error happens>' $Mail_Success = '<Your mail to the mailgroup>' $MailSMTP = '<Your SMTP-Server>' $MailPortnumber = '25' $MailCustomer = 'Name of company' ######################################################### # the function the extract the week number ######################################################### function Get-ISO8601Week (){ # Adapted from https://stackoverflow.com/a/43736741/444172 [CmdletBinding()] param( [Parameter( ValueFromPipeline = $true, ValueFromPipelinebyPropertyName = $true )] [datetime] $DateTime ) process { foreach ($_DateTime in $DateTime) { $_ResultObject = [pscustomobject] @{ Year = $null WeekNumber = $null WeekString = $null DateString = $_DateTime.ToString('yyyy-MM-dd dddd') } $_DayOfWeek = $_DateTime.DayOfWeek.value__ # In the underlying object, Sunday is always 0 (Monday = 1, ..., Saturday = 6) irrespective of the FirstDayOfWeek settings (Sunday/Monday) # Since ISO 8601 week date (https://en.wikipedia.org/wiki/ISO_week_date) is Monday-based, flipping Sunday to 7 and switching to one-based numbering. if ($_DayOfWeek -eq 0) { $_DayOfWeek = 7 } # Find the Thursday from this week: # E.g.: If original date is a Sunday, January 1st , will find Thursday, December 29th from the previous year. # E.g.: If original date is a Monday, December 31st , will find Thursday, January 3rd from the next year. $_DateTime = $_DateTime.AddDays((4 - $_DayOfWeek)) # The above Thursday it's the Nth Thursday from it's own year, wich is also the ISO 8601 Week Number $_ResultObject.WeekNumber = [math]::Ceiling($_DateTime.DayOfYear / 7) $_ResultObject.Year = $_DateTime.Year # The format requires the ISO week-numbering year and numbers are zero-left-padded (https://en.wikipedia.org/wiki/ISO_8601#General_principles) # It's also easier to debug this way :) $_ResultObject.WeekString = "$($_DateTime.Year)-W$("$($_ResultObject.WeekNumber)".PadLeft(2, '0'))" Write-Output $_ResultObject } } } ######################################################### # Section to extract monthname, year and weeknumbers ######################################################### $monthname = (Get-Culture).DateTimeFormat.GetMonthName((get-date).month) $year = (get-date).Year $week = Get-ISO8601Week (get-date) $nextweek = Get-ISO8601Week (get-date).AddDays(7) $weeknumber = $week.weeknumber $nextweeknumber = $nextweek.weeknumber ######################################################### #Calculate the numbers of days from todays date ######################################################### $limit = (get-date).AddDays($LimitDays) ######################################################### # Get the powershell module for MEMCM # and Send-Mailkitmessage ######################################################### if (-not(Get-Module -name ConfigurationManager)) { Import-Module ($Env:SMS_ADMIN_UI_PATH.Substring(0,$Env:SMS_ADMIN_UI_PATH.Length-5) + '\ConfigurationManager.psd1') } if (-not(Get-Module -name send-mailkitmessage)) { Install-Module send-mailkitmessage Import-Module send-mailkitmessage } ######################################################### # To run the script you must be on ps-drive for MEMCM ######################################################### Push-Location Set-Location $SiteCode ######################################################### # Array to collect result ######################################################### $Result = @() ######################################################### # Gather all updates in updategrpup ######################################################### $updates = Get-CMSoftwareUpdate -Fast -UpdateGroupName $UpdateGroupName Write-host "Processing Software Update Group" $UpdateGroupName forEach ($item in $updates) { $object = New-Object -TypeName PSObject $object | Add-Member -MemberType NoteProperty -Name ArticleID -Value $item.ArticleID #$object | Add-Member -MemberType NoteProperty -Name BulletinID -Value $item.BulletinID $object | Add-Member -MemberType NoteProperty -Name Title -Value $item.LocalizedDisplayName $object | Add-Member -MemberType NoteProperty -Name LocalizedDescription -Value $item.LocalizedDescription $object | Add-Member -MemberType NoteProperty -Name DatePosted -Value $item.Dateposted $object | Add-Member -MemberType NoteProperty -Name Deployed -Value $item.IsDeployed $object | Add-Member -MemberType NoteProperty -Name 'URL' -Value $item.LocalizedInformativeURL $object | Add-Member -MemberType NoteProperty -Name 'Required' -Value $item.NumMissing $object | Add-Member -MemberType NoteProperty -Name 'Installed' -Value $item.NumPresent $object | Add-Member -MemberType NoteProperty -Name 'Severity' -Value $item.SeverityName $result += $object } ######################################################### # Create a row in the email to present numbers of updates ######################################################### #$Numbersofupdates = "Totalt antal patchar från Microsoft som finns i Uppdateringspaketet " + $UpdateGroupName + " = " + $result.count $Numbersofupdates = "Total numbers of updates from Microsoft that exist in updatepackage " + $UpdateGroupName + " = " + $result.count ######################################################### # Create the list of updates sorted and only in the limit ######################################################### $UpdatesFound = $result | Sort-Object dateposted -Descending | where-object { $_.dateposted -ge $limit } ######################################################### # CSS HTML ######################################################### $header = @" <style> th { font-family: Arial, Helvetica, sans-serif; color: White; font-size: 12px; border: 1px solid black; padding: 3px; background-color: Black; } p { font-family: Arial, Helvetica, sans-serif; color: black; font-size: 12px; } ol { font-family: Arial, Helvetica, sans-serif; list-style-type: square; color: black; font-size: 12px; } tr { font-family: Arial, Helvetica, sans-serif; color: black; font-size: 11px; vertical-align: text-top; } body { background-color: #B1E2EC; } table { border: 1px solid black; border-collapse: collapse; } td { border: 1px solid black; padding: 5px; background-color: #E0F3F7; } </style> "@ ################################################################ # Check if the downloaded updates in the limit are zero or not ################################################################ if ($UpdatesFound -eq $null ) { write-host "No updates downloaded or deployed since $limit" #Emailsettings when updates equals none $EmailTo = $Mail_Error $UpdatesFound = @" <br> <img src='cid:logo.png' height="50"> <br> <B>No updates downloaded or deployed since $limit</B><br><br> <p></p> <p>Action needed from third-line support</p> "@ } else { ######################################################### # Text added to mail before list of patches ######################################################### ######################################################### # Emailsettings when updates more then one downloaded ######################################################### $EmailTo = $Mail_Success ######################################################### # The top of the email ######################################################### $pre = @" <br> <img src='cid:logo.png' height="50"> <br> <p><b>New updates!</b><br> <p>Updates will be available from wednesday week $weeknumber kl.15.00</p> <p><b>Schema</b><br> <p>The updates will be installed as follows:</p> <p><ol>Test - Week $weeknumber - Every night between 03.00 - 08:00 (If any updates are published)</ol></p> <p><ol>Prod - Week $nextweeknumber - Majority will be installed saturday 11.00pm till Sunday 09.00am</ol></p> <p><ol>AX - Managed manually by the administration.</ol></p> <p><b>Patchar From Microsoft</b><br> <p>The following updates are downloaded and published in updategroup <b><i>$UpdateGroupName</i></b> since $limit</p> <p>$Numbersofupdates</p> "@ ######################################################### # Footer of the email ######################################################### $post = @" <p>Raport created $((Get-Date).ToString()) from <b><i>$($Env:Computername)</i></b></p> <p>Script created by:<br><a href="mailto:Your Email">Your name</a><br> <a href="https://your blog">your description of your blog</a> "@ ########################################################################################## # Mail with pre and post converted to Variable later used to send with send-mailkitmessage ########################################################################################## $UpdatesFound = $result | Sort-Object dateposted -Descending | where-object { $_.dateposted -ge $limit }| ConvertTo-Html -Title "Downloaded patches" -PreContent $pre -PostContent $post -Head $header } ######################################################### # Mailsettings # using module Send-MailKitMessage ######################################################### #use secure connection if available ([bool], optional) $UseSecureConnectionIfAvailable=$false #authentication ([System.Management.Automation.PSCredential], optional) $Credential=[System.Management.Automation.PSCredential]::new("Username", (ConvertTo-SecureString -String "Password" -AsPlainText -Force)) #SMTP server ([string], required) $SMTPServer=$MailSMTP #port ([int], required) $Port=$MailPortnumber #sender ([MimeKit.MailboxAddress] http://www.mimekit.net/docs/html/T_MimeKit_MailboxAddress.htm, required) $From=[MimeKit.MailboxAddress]$MailFrom #recipient list ([MimeKit.InternetAddressList] http://www.mimekit.net/docs/html/T_MimeKit_InternetAddressList.htm, required) $RecipientList=[MimeKit.InternetAddressList]::new() $RecipientList.Add([MimeKit.InternetAddress]$MasailTo) #cc list ([MimeKit.InternetAddressList] http://www.mimekit.net/docs/html/T_MimeKit_InternetAddressList.htm, optional) #$CCList=[MimeKit.InternetAddressList]::new() #$CCList.Add([MimeKit.InternetAddress]$EmailToCC) #bcc list ([MimeKit.InternetAddressList] http://www.mimekit.net/docs/html/T_MimeKit_InternetAddressList.htm, optional) $BCCList=[MimeKit.InternetAddressList]::new() $BCCList.Add([MimeKit.InternetAddress]"BCCRecipient1EmailAddress") # Different subject depending on result of search for patches. if ($UpdatesFound -ne $null ) { #subject ([string], required) $Subject=[string]"Serverpatchning $MailCustomer $monthname $year" } else { #subject ([string], required) $Subject=[string]"Error Error - Action needed $(get-date)" } #text body ([string], optional) #$TextBody=[string]"TextBody" #HTML body ([string], optional) $HTMLBody=[string]$UpdatesFound #attachment list ([System.Collections.Generic.List[string]], optional) $AttachmentList=[System.Collections.Generic.List[string]]::new() $AttachmentList.Add("$PSScriptRoot\logo.png") # Mailparameters $Parameters=@{ "UseSecureConnectionIfAvailable"=$UseSecureConnectionIfAvailable #"Credential"=$Credential "SMTPServer"=$SMTPServer "Port"=$Port "From"=$From "RecipientList"=$RecipientList #"CCList"=$CCList #"BCCList"=$BCCList "Subject"=$Subject #"TextBody"=$TextBody "HTMLBody"=$HTMLBody "AttachmentList"=$AttachmentList } ######################################################### #send email ######################################################### Send-MailKitMessage @Parameters<span id="ib-toc-anchor-5"></span> |
Step Three – Send-UpdateStatusMail.ps1
This script generates an Email with information of success and failure of updates in a specific Update Group.
To configure the script you need to set:
- Limitday – Numbers of days back in time for the deployment
- Sitecode – Your sitecode
- UpdateGroupName – Update Group Name
- EmailFrom – Your no-reply address
- EmailTo – Group or user address
- EmailCustomer – Name of Company
- EmailSMTP – Your smtp-server
- EmailPort – Your portnumber for mailserver (Default 25)
Your also need to customize the Email and for that you need some html-knowledge (You can find the most on Google 🙂 )
Script requirements
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# #.Synopsis # Creates a html-mail with list on downloaded and deployed patches from Microsoft in a # updatepackage selected in the script. #.DESCRIPTION # Query MECM for updates downloaded to a specific update-package. In the config-file you select how many days # back from todays date you want to check. # Skripts require Powershell 7 and send-mailkitmessage module https://www.powershellgallery.com/packages/Send-MailKitMessage #.EXAMPLE # Send-UpdateStatusMail.ps1 ###################################################################### # Configuration for the script ###################################################################### $LimitFrom = '-10' $SiteCode = '<Sitecode:>' $UpdateGroupName = '<UpdateGroupName>' $Emailfrom = '<Your no-replay address> $EmailTo = '<Recipients mailgroupaddress>' $EmailCustomer = '<the name of your customer>' $EmailSmtp = '<your smtpserver>' $mailport = '25' #Calculate the numbers of days from todays date $limit = (get-date).AddDays($LimitFromXML) # Get the powershell module for MEMCM if (-not(Get-Module -name ConfigurationManager)) { Import-Module ($Env:SMS_ADMIN_UI_PATH.Substring(0,$Env:SMS_ADMIN_UI_PATH.Length-5) + '\ConfigurationManager.psd1') } # Get the powershell module for Send-MailKitMessage if (-not(Get-Module -name Send-Mailkitmessage)) { Install-Module -Name Send-MailKitMessage } #Get-InstalledModule # To run the script you must be on ps-drive for MEMCM Push-Location Set-Location $SiteCode # Array to collect result $Result = @() $resultat = Get-CMDeployment | Where-Object softwarename -like '*server*' foreach ($item in $resultat) { $object = New-Object -TypeName PSObject $object | Add-Member -MemberType NoteProperty -Name 'Collection' -Value $item.Collectionname $object | Add-Member -MemberType NoteProperty -Name 'Last date' -Value $item.SummarizationTime $object | Add-Member -MemberType NoteProperty -Name 'UpdateGroupName' -Value $item.ApplicationName $object | Add-Member -MemberType NoteProperty -Name 'Errors' -Value $item.NumberErrors $object | Add-Member -MemberType NoteProperty -Name 'In Progress' -Value $item.NumberInProgress $object | Add-Member -MemberType NoteProperty -Name 'Success' -Value $item.NumberSuccess $object | Add-Member -MemberType NoteProperty -Name 'Targeted' -Value $item.NumberTargeted $object | Add-Member -MemberType NoteProperty -Name 'Unknown' -Value $item.NumberUnknown $result += $object } $Title = "Total numbers of deployments in list are " + $result.count # CSS HTML $header = @" <style> th { font-family: Arial, Helvetica, sans-serif; color: White; font-size: 14px; border: 1px solid black; padding: 3px; background-color: Black; } p { font-family: Arial, Helvetica, sans-serif; color: black; font-size: 12px; } tr { font-family: Arial, Helvetica, sans-serif; color: black; font-size: 12px; vertical-align: text-top; } body { background-color: white; } table { border: 1px solid black; border-collapse: collapse; } td { border: 1px solid black; padding: 5px; background-color: #e6f7d0; } </style> "@ ###################################################################### # Text added to mail before list of patches ###################################################################### $pre = @" <img src='cid:logo.png' height="50"> <p>This report list status for updatedeployments from site $SiteCodeFromXML owned by $EmailCustomer.</p> <p>$title</p> "@ ###################################################################### # Text added to mail last ###################################################################### $post = "<p>Report generated on $((Get-Date).ToString()) from <b><i>$($Env:Computername)</i></b></p>" ###################################################################### # Mail with pre and post converted to Variable later used to send with send-mailkitmessage ###################################################################### $StatusFound = $result | Sort-Object dateposted -Descending | ConvertTo-Html -Title "Downloaded patches" -PreContent $pre -PostContent $post -Head $header ## Mailsettings # using module Send-MailKitMessage #use secure connection if available ([bool], optional) $UseSecureConnectionIfAvailable=$false #authentication ([System.Management.Automation.PSCredential], optional) $Credential=[System.Management.Automation.PSCredential]::new("Username", (ConvertTo-SecureString -String "Password" -AsPlainText -Force)) #SMTP server ([string], required) $SMTPServer=$EmailSmtp #port ([int], required) $Port=$mailport #sender ([MimeKit.MailboxAddress] http://www.mimekit.net/docs/html/T_MimeKit_MailboxAddress.htm, required) $From=[MimeKit.MailboxAddress]$Emailfrom #recipient list ([MimeKit.InternetAddressList] http://www.mimekit.net/docs/html/T_MimeKit_InternetAddressList.htm, required) $RecipientList=[MimeKit.InternetAddressList]::new() $RecipientList.Add([MimeKit.InternetAddress]$EmailTo) #cc list ([MimeKit.InternetAddressList] http://www.mimekit.net/docs/html/T_MimeKit_InternetAddressList.htm, optional) $CCList=[MimeKit.InternetAddressList]::new() $CCList.Add([MimeKit.InternetAddress]$EmailToCC) #bcc list ([MimeKit.InternetAddressList] http://www.mimekit.net/docs/html/T_MimeKit_InternetAddressList.htm, optional) $BCCList=[MimeKit.InternetAddressList]::new() $BCCList.Add([MimeKit.InternetAddress]"BCCRecipient1EmailAddress") #subject ([string], required) $Subject=[string]"$Emailcustomer - Status deployments $(get-date)" #text body ([string], optional) $TextBody=[string]"TextBody" #HTML body ([string], optional) $HTMLBody=[string]$StatusFound #attachment list ([System.Collections.Generic.List[string]], optional) $AttachmentList=[System.Collections.Generic.List[string]]::new() $AttachmentList.Add("$PSScriptRoot\logo.png") # Mailparameters $Parameters=@{ "UseSecureConnectionIfAvailable"=$UseSecureConnectionIfAvailable #"Credential"=$Credential "SMTPServer"=$SMTPServer "Port"=$Port "From"=$From "RecipientList"=$RecipientList #"CCList"=$CCList #"BCCList"=$BCCList "Subject"=$Subject #"TextBody"=$TextBody "HTMLBody"=$HTMLBody "AttachmentList"=$AttachmentList } ###################################################################### #send message ###################################################################### Send-MailKitMessage @Parameters<span id="ib-toc-anchor-7"></span> |
Step four – Schedule task to send Mail
In order to send emails at a specific time after patch Tuesday, I needed to create this script. The built-in Schedule Task didn’t have the offsetdays feature I needed to get what I wanted.
The only downside right now is that if you send out updates in each month and want mail out every month, there will be twelve Schedule Tasks.
Then once every year you need to execute the script to remove old jobs and post new ones, the same as you get to do for the Maintenance Windows script.
Script requirements
Note: The script will fail if you run it with Powershell 7.x, it will fail on “-Trigger” |
The parameters you need to provide:
- Offsetweeks – Weeks after Patch Tuesday
- OffsetDays – Days after Patch Tuesday
- AddStarthour – Hour to start Schedule Task (0-23)
- AddStartMinutes – Minutes to Start Schedule Task (0-59)
- PatchMonth – Which month to run Schedule Task (1-12)
- PatchYear – Which year to run Schedule Task
- FolderName – Name of folder in Schedule Task where the Jobs will be created
- UserName – The user to run Schedule Task
- Execute – pwsh.exe or Powershell.exe.. or what ever you want to use
- Scriptpath – Where the script that you use in the Schedule Task
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
<# .SYNOPSIS This script creates x-numbers of scheduled task based on when patch tuesday occurs. .DESCRIPTION This script give you the option to create multiple scheduled task to send a mail x-days after patch tuesday. ############################################################ Christian Damberg www.damberg.org Version 1.0 2021-12-22 ############################################################ .EXAMPLE .\Set-ScheduleTaskPatchTuesday.ps1 -OffSetWeeks 0 -OffSetDays 2 -AddStartHour 12 -AddStartMinutes 0 -PatchMonth "1","2","3","4","5","6","7","8","9","10","11" -patchyear 2022 -FolderName PatchMail -execute 'pwsh.exe' -scriptpath 'C:\Scripts\PatchManagement\Send-UpdateDeployedMail.ps1' -UserName 'Korsberga.local\scriptrunner' -Verbose Will create Schedule Task for January to November to send mail two days after patch tuesday at noon. .DISCLAIMER All scripts and other Powershell references are offered AS IS with no warranty. These script and functions are tested in my environment and it is recommended that you test these scripts in a test environment before using in your production environment. #> ############################################################ # Parameters ############################################################ PARAM( [int]$OffSetWeeks, [int]$OffSetDays, [Parameter(Mandatory=$True)] [int]$AddStartHour, [Parameter(Mandatory=$True)] [int]$AddStartMinutes, [string[]]$PatchMonth, [Parameter(Mandatory=$True)] [int]$patchyear, [string]$FolderName, [string]$UserName, [string]$execute, [string]$scriptpath ) ############################################################ # region functions ############################################################ $password = read-host -Prompt "The domain password for $username" # Read passwordfile #$Encrypted = Get-Content $PathPasswordFile | ConvertTo-SecureString #$UnsecurePassword = (New-Object PSCredential "user",$encrypted).GetNetworkCredential().Password # Create variable with username and password #$Credential = New-Object System.Management.Automation.PsCredential($UserName, $Encrypted) # Set Patch Tuesday for a Month Function Get-PatchTuesday ($Month,$Year) { $FindNthDay=2 #Aka Second occurence $WeekDay='Tuesday' $todayM=($Month).ToString() $todayY=($Year).ToString() $StrtMonth=$todayM+'/1/'+$todayY [datetime]$StrtMonth=$todayM+'/1/'+$todayY while ($StrtMonth.DayofWeek -ine $WeekDay ) { $StrtMonth=$StrtMonth.AddDays(1) } $PatchDay=$StrtMonth.AddDays(7*($FindNthDay-1)) return $PatchDay Write-Log -Message "Patch Tuesday this month is $PatchDay" -Severity 1 -Component "Set Patch Tuesday" Write-Output "Patch Tuesday this month is $PatchDay" } Set-Location $PSScriptRoot # Function for append events to logfile located c:\windows\logs Function Write-Log { PARAM( [String]$Message, [int]$Severity, [string]$Component ) Set-Location $PSScriptRoot $Logpath = "C:\Windows\Logs" $TimeZoneBias = Get-CimInstance win32_timezone $Date= Get-Date -Format "HH:mm:ss.fff" $Date2= Get-Date -Format "MM-dd-yyyy" "<![LOG[$Message]LOG]!><time=$([char]34)$Date$($TimeZoneBias.bias)$([char]34) date=$([char]34)$date2$([char]34) component=$([char]34)$Component$([char]34) context=$([char]34)$([char]34) type=$([char]34)$Severity$([char]34) thread=$([char]34)$([char]34) file=$([char]34)$([char]34)>"| Out-File -FilePath "$Logpath\Create-ScheduleTask.log" -Append -NoClobber -Encoding default } # Function to create a folder in Scheduled Task Function New-ScheduledTaskFolder { Param ($taskpath) $ErrorActionPreference = "stop" $scheduleObject = New-Object -ComObject schedule.service $scheduleObject.connect() $rootFolder = $scheduleObject.GetFolder("\") Try {$null = $scheduleObject.GetFolder($taskpath)} Catch { $null = $rootFolder.CreateFolder($taskpath) } Finally { $ErrorActionPreference = "continue" } } # Misc variables needed in script $ErrorMessage = $_.Exception.Message $MonthArray = New-Object System.Globalization.DateTimeFormatInfo $MonthNames = $MonthArray.MonthNames # Create a subfolder in Scheduled Task New-ScheduledTaskFolder $FolderName # Create Scheduled Task for for every month specified in variable Patchmonth foreach ($Monthnumber in $PatchMonth) { # Set Patch Tuesday for each Month $PatchDay = Get-PatchTuesday $Monthnumber $PatchYear # Set month number correct to display name later in script (Months array starting from 0 hence the -1) $displaymonth = $Monthnumber - 1 # Set starttime for schedule task $StartTime=$PatchDay.AddDays($OffSetDays).AddHours($AddStartHour).AddMinutes($AddStartMinutes) ############################################################ # This section must be edited before running the script ############################################################ # Action in Scheduled Task $taskAction = New-ScheduledTaskAction ` -Execute $execute ` -Argument "-File $scriptpath -ExecutionPolicy bypass" ############################################################ # Done ############################################################ # Create a new trigger (Daily at 3 AM) $tasktrigger = New-ScheduledTaskTrigger -At $StartTime -Once # The name of your scheduled task. $taskName = "Patchstatus-Mail " +$MonthNames[$displaymonth] + " "+ $patchyear # Describe the scheduled task. $description = "Mail - Status on downloaded and deployed patches" $Taskusername = $Credential.UserName $TaskPwd = $Credential.Password Try { # Register the scheduled task Register-ScheduledTask -TaskName $taskName -Action $taskAction -Trigger $taskTrigger -Description $description -TaskPath $FolderName -User $username -Password $password -RunLevel Highest #Write-Log -Message "Created schedule task $taskname " -Severity 1 -Component "New Schedule Task" } Catch { Write-Warning "$_.Exception.Message" Write-Log -Message "$_.Exception.Message" -Severity 3 -Component "Create Schedule Task" } } Set-Location $PSScriptRoot |
You must be logged in to post a comment.