Thursday, April 6, 2017

Yet another Auto-Update script...

NOTE: Updates are now being posted here: https://www.powershellgallery.com

Here again is my take on automatic updating.  We have a guy that handles patching via a 3rd party tool (Kaspersky).  I don't like him touching my domain controllers so I used a number of scripts I found to distill down to the script below.

Kaspersky uses a network agent to keep the settings uniform and that causes issues when you want to use the Microsoft update site.  This script is designed to stop this agent temporarily and allow normal patching to occur.

It requires the NuGet and PSWindowsUpdate modules but will auto-load them if needed.  It also requires PowerShell v5.

To simplify the entire process I decided that the script should run twice, once Saturday night to apply patches, then again Sunday morning to verify what was done.  The verification sends and html coded report  to whoever you chose.  It also creates a .CVS log file and only keeps the last 10 to avoid filling the disk.

So far this seems to work quite well.  It will FORCE a reboot to be sure the patches get in right so understand that before you run it. 

This script uses no external config file so you need to edit the internal config lines.  It sets up scheduled tasks to re-run itself on days noted in the config file.  If it sees the tasks don't exist it will create them.  It sets them up to run using the LOCAL system context.

I added the ability to deploy the script to other systems listed in the config file.  That helps get all instances to the same version.

Please test this thoroughly before running on any production systems.  My setup will undoubtedly be different from yours and this was built to pause Kaspersky before it will run.

Command line options for console output and status only emails are documented within the script.

Note that I use encrypted credentials stored in the config file.  The example below has bogus data.

(12-01-17: Updated to v2.5 below to fix a minor bug...)

param(
    [bool]$Console = $False,        #--[ Set to true to enable local console result display. Defaults to false ]--
    [bool]$Debug = $False,          #--[ Set to true to only send results to debug email address. Default to false ]--
    [bool]$Manual = $False,         #--[ Use to run update off-schedule ]--
    [bool]$Status = $False,         #--[ If set to true checks for results after the reboot and emails, then goes idle. ]--
    [bool]$Deploy = $False          #--[ If set to true will copy this script to the other members of the peer group ]--  
    )
<#======================================================================================
          File Name : UnattendedUpdate.ps1
    Original Author : Kenneth C. Mazie (kcmjr AT kcmjr.com)
                    :
        Description : Will scan the Windows Update site and install all missing updates.
                    :
          Operation : Requires PowerShell v5.  Requires NuGet and PSWindowsUpdate
                    : modules.  Will auto-install them if needed.  Reboots system if Powershell
                    : determines it is required.  Creates appropriate LOCAL scheduled
                    : tasks automatically on first run. Schedules are set to randomize run
                    : within a 90 minute window.  Update routine is governed by the week days
                    : noted in the config fileRequires a config file in XML format located
                    : in the same folder as the script.  See example at bottom.
                    :
          Arguments : Normal operation is with no command line options.
                    : -Console $true  (Will enable local console output)
                    : -Debug $true    (Not used)
                    : -Manual $true   (forces a manual run bypassing the schedule) 
                    : -Status $true   (forces a status email to be sent)
                    : -Deploy $true   (forces the script and config file to be copied to the identical
                    :                  location on the other peer servers listed in the config file)
                    :
           Warnings : Uses local SYSTEM user context for tasks.  Install LOCALLY, not remotely.
                    : Adjust the task schedule(s) to conform to your maintenance window.
                    :  
              Legal : Public Domain. Modify and redistribute freely. No rights reserved.
                    : SCRIPT PROVIDED "AS IS" WITHOUT WARRANTIES OR GUARANTEES OF
                    : ANY KIND. USE AT YOUR OWN RISK. NO TECHNICAL SUPPORT PROVIDED.
                    :
            Credits : Code snippets and/or ideas came from many sources including but
                    :   not limited to the following:
                    : https://www.powershellgallery.com/packages/PSWindowsUpdate/1.5.2.2
                    :
     Last Update by : Kenneth C. Mazie (kcmjr AT kcmjr.com)    
                     #>
     $Script:ScriptVer = "2.5"
                    <#
    Version History : v1.0 - 12-29-16 - Original
     Change History : v2.0 - 01-13-17 - Added forced status option for Sundays.
                    : v2.1 - 01-17-17 - Added regkey delete.
                    : v2.2 - 04-06-17 - Fixed rowdata to start clean at each loop
                    : v2.3 - 09-28-17 - Turned off extra email after patching.   
                    :   Moved config file out.    Added script replication option.
                    :    Added run day from config file option.
                    : v2.4 - 10-19-17 - Added randomizer for reboot. Added check for no data.
                    : v2.5 - 12-01-17 - Fixed runday detection
                    :
=======================================================================================#>
#Requires -version 5.0
clear-host

if ($Console){$Script:Console = $true}
if ($Debug){$Script:Debug = $true}
if ($Manual){$Script:Manual = $true}
if ($Status){$Script:Status = $true}
if ($Deploy){
    $Script:Deploy = $true
    $Script:Console = $true
}

$ErrorActionPreference = "SilentlyContinue"
$Now = Get-Date -Format "MM-dd-yyyy_HHmm"
$Script:ThisComputer = $Env:Computername
$Script:MessageBody = "Starting Local Patch Processing...<br>"
#--------------------------------------
$Today = (get-date).dayofweek
$Script:Attach = $false
$Script:ResultLog = "$PSScriptRoot\Results-$Now.csv"
$Script:KillKaspersky = $true  #--[ Used to stop the Kaspersky agent during updates.  Set to $false if not applicable ]--
$Script:ScriptName = $MyInvocation.MyCommand.Name
$Script:ScriptFullPath = $PSScriptRoot+"\"+$MyInvocation.MyCommand.Name
$ConfigFile = $Script:ScriptFullPath.Split(".")[0]+".xml"

$Script:UserContext = [Security.Principal.WindowsIdentity]::GetCurrent()

#==[ Functions ]================================================================

function LoadConfig {
#--[ Read and load configuration file ]-------------------------------------
    if (!(Test-Path $ConfigFile)){                       #--[ Error out if configuration file doesn't exist ]--
          $Script:HTMLData = "MISSING CONFIG FILE.  Script aborted."
        if ($Script:Log){Add-content -Path "$PSScriptRoot\debug.txt" -Value "MISSING CONFIG FILE.  Script aborted."}
           write-host "CONFIGURATION FILE $ConfigFile NOT FOUND - EXITING" -ForegroundColor Red
          break
    }else{
        [xml]$Script:Configuration = Get-Content $ConfigFile  #--[ Read & Load XML ]--   
        $Script:PeerList = $Script:Configuration.Settings.General.PeerList
        $Script:RunDays = $Script:Configuration.Settings.General.RunDays
        $Script:RunTime = $Script:Configuration.Settings.General.RunTime
        $Script:DebugEmail = $Script:Configuration.Settings.Email.Debug
        $Script:eMailRecipient = $Script:Configuration.Settings.Email.To
        $Script:eMailFrom = $ThisComputer+'_'+$Script:Configuration.Settings.Email.From   
        $Script:eMailHTML = $Script:Configuration.Settings.Email.HTML
        $Script:eMailSubject = $ThisComputer+' '+($Script:Configuration.Settings.Email.Subject)
        $Script:SmtpServer = $Script:Configuration.Settings.Email.SmtpServer
        $Script:UserName = $Script:Configuration.Settings.Credentials.Username
        $Script:EncryptedPW = $Script:Configuration.Settings.Credentials.Password
        $Script:Base64String = $Script:Configuration.Settings.Credentials.Key
        $Script:ReportName = $ThisComputer+' '+($Script:Configuration.Settings.General.ReportName)
    }

    $ByteArray = [System.Convert]::FromBase64String($Script:Base64String);
    $Script:Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Script:UserName, ($Script:EncryptedPW | ConvertTo-SecureString -Key $ByteArray)
    $Script:Password = $Script:Credential.GetNetworkCredential().Password
}


function SendEmail {
    $SMTP = new-object System.Net.Mail.SmtpClient($Script:SmtpServer)
    $Email = New-Object System.Net.Mail.MailMessage
    $Email.Body = $Script:MessageBody
    $Email.IsBodyHtml = $Script:eMailHTML
    $Email.To.Add($Script:eMailRecipient)
    if ($Script:Attach){
        $Attachment = New-Object System.Net.Mail.AttachmentArgumentList $Script:ResultLog, 'Application/Octet'
        $Email.Attachments.Add($Attachment)
    }   
    $Email.From = $Script:eMailFrom
    $Email.Subject = $Script:eMailSubject
    $SMTP.Send($Email)
    $Email.Dispose()
    $SMTP.Dispose()
}

function GetResults {
    $Script:ResultOut = ""
    $Script:ResultLine = ""
    $RowFlag = $false
    #--[ Add header to html output file ]--
#    $Script:HTMLData = @()
    $Script:MessageBody += '<tr><th>KB Number</th><th>Status</th><th>Date / Time</th><th>Results</th><th>Messages</th></tr>'
    #--[ HTML row Settings ]----------------------------------------------------
    $BGColor = "#dfdfdf"                                                    #--[ Grey default cell background ]--
    $BGColorRed = "#ff0000"                                                 #--[ Red background for alerts ]--
    $BGColorOra = "#ff9900"                                                 #--[ Orange background for alerts ]--
    $BGColorYel = "#ffd900"                                                 #--[ Yellow background for alerts ]--
    $FGColor = "#000000"                                                    #--[ Black default cell foreground ]--
   
    #--[ Only keep 10 of the last runtime logs ]------------------------------------
    Get-ChildItem -Path $PSScriptRoot | Where-Object {(-not $_.PsIsContainer) -and ($_.Name -like "*results*.csv")} | Sort-Object -Descending -Property LastTimeWrite | Select-Object -Skip 10 | Remove-Item
   
    #--[ Scan Eventlogs for Event 19 & 20 ]--
    $Script:LogDump = Get-WinEvent -FilterHashtable @{LogName = "System";ID=19,20}
    foreach ($Script:LogItem in $Script:LogDump ){
          if ($Script:LogItem.ProviderName -eq 'Microsoft-Windows-WindowsUpdateClient'){
            $RowFlag = $true
            $RowData = '<tr>'                                                        #--[ Start table row ]--
              $Script:LogItemStat = $Script:LogItem.message.split(":")[0]
              if ($Script:LogItem.Message -like "*(KB*"){
                  $Script:LogItemKB = ($Script:LogItem.message.split("(")[1]).split(")")[0]
                  if (!($Script:LogItemKB -like "KB*")){
                      $Script:LogItemKB = ($Script:LogItem.message.split("(")[2]).split(")")[0]
                  }
              }else{
                  $Script:LogItemKB = $Script:LogItem.message.split(":")[2] #"---------"
              } 
              if ($Script:Console){write-host $Script:LogItemKB"   " -ForegroundColor Red -NoNewline }
            $RowData += '<td bgcolor=' + $BGColor + '><font color=' + $FGColor + '>' + $Script:LogItemKB + '</td>'
           
              if ($Script:Console){write-host $Script:LogItem.LevelDisplayName"     " -ForegroundColor yellow -NoNewline }
            if ($Script:LogItem.LevelDisplayName -like "Error"){
                   $RowData += '<td bgcolor=' + $BGColor + '><font color=#800000>' + $Script:LogItem.LevelDisplayName + '</td>'
                $RowData += '<td bgcolor=' + $BGColor + '><font color=#800000>' + $Script:LogItemStat + '</td>'
            }else{   
                $RowData += '<td bgcolor=' + $BGColor + '><font color=' + $FGColor + '>' + $Script:LogItem.LevelDisplayName + '</td>'
                $RowData += '<td bgcolor=' + $BGColor + '><font color=' + $FGColor + '>' + $Script:LogItemStat + '</td>'
            }
           
            if ($Script:Console){write-host $Script:LogItem.TimeCreated"     " -ForegroundColor magenta -NoNewline }
              $RowData += '<td bgcolor=' + $BGColor + '><font color=' + $FGColor + '>' + $Script:LogItem.TimeCreated + '</td>'
           
            if ($Script:Console){write-host $Script:LogItemStat"     " -ForegroundColor Cyan -NoNewline }
              #$RowData += '<td bgcolor=' + $BGColor + '><font color=#800000>' + $Script:LogItemStat + '</td>'   #--[ Note: Added to error detection above ]--
           
            if ($Script:Console){write-host $Script:LogItem.Message"     " -ForegroundColor green}
            $RowData += '<td bgcolor=' + $BGColor + '><font color=' + $FGColor + '>' + $Script:LogItem.message + '</td>'
           
              $Script:ResultLine = $Script:LogItemKB+","+$Script:LogItem.LevelDisplayName+","+$Script:LogItem.TimeCreated+","+$Script:LogItemStat+","+$Script:LogItem.Message
              $Script:ResultOut = $Script:ResultOut+$Script:ResultLine+"`n" 
            $RowData += '</tr>'
        }
        $Script:MessageBody += $RowData
    }

    If ($RowFlag){
           $Script:Attach = $true
    }Else{
           $Script:MessageBody += '<td colspan=5 bgcolor=' + $BGColor + '><font color=' + $FGColor + '><center>No New Data to Report </center></td>' 
    }
   
       Add-Content -value $Script:ResultOut -path $Script:ResultLog
    $Script:MessageBody += '</table><br>'
       $Script:MessageBody += "<font size=3 face='times new roman'>- Done.  Emailing results...<br>"
    if ($Script:Console){Write-Host "`n- Done.  Emailing results...`n" -ForegroundColor Magenta }
    SendEmail
}

function ServiceMgr ($Svc, $SvcStatus) {
    $Script:MessageBody = $Script:MessageBody + "- Processing :$Svc<br>"
    if ($Script:Console){Write-Host "- Processing :$Svc" -ForegroundColor Magenta }
    $Count = 0
    #--[ State prior to start/stop process ]--
    $Script:SvcState = (Get-Service -Name $Svc).Status
    $Script:MessageBody = $Script:MessageBody + "-- $Svc Initial Status: $Script:SvcState<br>"
    if ($Script:Console){Write-Host "-- $Svc Initial Status: $Script:SvcState" -ForegroundColor Magenta }
    $Script:MessageBody = $Script:MessageBody + "--- Pausing while attemtping to set service state to: $SvcStatus...<br>"
    if ($Script:Console){Write-Host "--- Pausing while attemtping to set service state to: $SvcStatus..." -ForegroundColor Magenta }
    while ((Get-Service -Name $Svc).Status -ne $SvcStatus){
        Get-Service -Name $Svc | Set-Service -Status $SvcStatus       
        sleep -Milliseconds 500
        $Count ++
        if ($Count -ge 20){
            if ($Script:Console){Write-Host "-- There was an error setting the "$Svc" service to the "$SvcStatus" state..." -ForegroundColor Magenta }
            $Script:MessageBody = $Script:MessageBody + '-- There was an error setting the "$Svc" service to the "$SvcStatus" state...<br>'
            break
        }
    }
    #--[ State after start/stop process ]--
    $Script:SvcState = (Get-Service -Name $Svc).Status
    $Script:MessageBody = $Script:MessageBody + "-- $Svc Final Status: $Script:SvcState<br>"
    if ($Script:Console){Write-Host "-- $Svc Final Status: $Script:SvcState" -ForegroundColor Magenta }
    Sleep -Seconds 1
    if ($Script:SvcState -eq "stopped"){
        RegKill
    }
}

function RegKill {
    if ($Script:Console){Write-Host "-- Removing Windows Update registry key..." -ForegroundColor Magenta }
    try{
        Remove-ItemProperty -Name 'WUServer' -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Force:$true -Confirm:$false
        Remove-ItemProperty -Name 'WUStatusServer' -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Force:$true -Confirm:$false
        Remove-Item -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Force:$true -Confirm:$false -Recurse:$true
    }catch{
            $ErrorMessage = $_.Exception.Message
        $FailedItem = $_.Exception.ItemName
        $Script:MessageBody = $Script:MessageBody + "- Failed to remove Windows Update Key(s).   $ErrorMessage<br>"
        if ($Script:Console){Write-Host "- Failed to remove Windows Update Key(s).   $ErrorMessage" -ForegroundColor Red }
    }
    if (Test-Path -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'){
        if ($Script:Console){Write-Host "-- FAILED to remove Windows Update registry key..." -ForegroundColor Magenta }
        $Script:MessageBody = $Script:MessageBody + "- Failed to remove Windows Update Key(s).<br>"
    }else{
        if ($Script:Console){Write-Host "-- Verified removal of Windows Update registry key..." -ForegroundColor Magenta }
        $Script:MessageBody = $Script:MessageBody + "- Verified removal of Windows Update registry key(s).<br>"
    }
}

function PatchIt {
    if ($Script:KillKaspersky){
        ServiceMgr "klnagent" "stopped"  #--[ Stops Kaspersky agent prior to running update.  Comment out if not applicable. ]--
    }else{
        $SvcState = "stopped"
    }
    if ($SvcState -eq "stopped"){
        #--[ NuGet is required to pull the module from the MS repository ]--
        if (!(Get-PackageProvider NuGet)){
            if (!(Get-ChildItem -Path "C:\Program Files\PackageManagement\ProviderAssemblies\nuget" -Filter "Microsoft.PackageManagement.NuGetProvider.dll" -Recurse)){
                try{
                    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -ErrorAction SilentlyContinue -Confirm:$false -Force:$true
                    $Script:MessageBody = $Script:MessageBody + "- NuGet provider is being installed.<br>"
                    if ($Script:Console){Write-Host "- NuGet provider is being installed." -ForegroundColor Magenta }
                }catch{
                    $ErrorMessage = $_.Exception.Message
                    $FailedItem = $_.Exception.ItemName
                    $Script:MessageBody = $Script:MessageBody + "- NuGet module install FAILED.   $ErrorMessage<br>"
                    if ($Script:Console){Write-Host "- NuGet module install FAILED.   $ErrorMessage" -ForegroundColor Red }
                }   
            }
        }

        #--[ Install the update module if it's not already loaded ]--
        if (!(Get-Module PSWindowsUpdate)){
            try{
                $Script:MessageBody = $Script:MessageBody + '- "PSWindowsUpdate" module is being installed.<br>'
                if ($Script:Console){Write-Host '- "PSWindowsUpdate" module is being installed...' -ForegroundColor Magenta }
                Install-Module PSWindowsUpdate -ErrorAction Stop -Confirm:$false -Force:$true
            }catch{   
                $ErrorMessage = $_.Exception.Message
                $FailedItem = $_.Exception.ItemName
                $Script:MessageBody = $Script:MessageBody + '- "PSWindowsUpdate" module install FAILED.   $ErrorMessage<br>'
                if ($Script:Console){Write-Host '- "PSWindowsUpdate" module install FAILED.   $ErrorMessage' -ForegroundColor Red }
            }
        }

        #--[ Register to use the Microsoft Update Service, as opposed to just the default Windows Update Service. ]--
        if (!((Get-WUServiceManager).ServiceID -contains "7971f918-a847-4430-9279-4a52d1efe18d")){
            try{
                $Script:MessageBody = $Script:MessageBody + "- Service Manager ID is being registered.<br>"
                if ($Script:Console){Write-Host "- Service Manager ID is being registered." -ForegroundColor Magenta }
                Add-WUServiceManager -ServiceID '7971f918-a847-4430-9279-4a52d1efe18d' -Confirm:$false -ErrorAction Stop
            }catch{
                $ErrorMessage = $_.Exception.Message
                $FailedItem = $_.Exception.ItemName
                $Script:MessageBody = $Script:MessageBody + "- Service Manager ID registration FAILED.  $ErrorMessage<br>"
                if ($Script:Console){Write-Host "- Service Manager ID registration FAILED.  $ErrorMessage" -ForegroundColor Red }
            }   
        }

        #--[ Run the update in unattended mode, check for all updates on the MS update site, accept all EULAs, reboot if needed ]--
        $Script:MessageBody = $Script:MessageBody + "- Checking for and installing new updates.<br>- Because a reboot is required to register new patches, results are emailed following every reboot.  The system will reboot shortly."
        if ($Script:Console){Write-Host "- Checking for and installing new updates.  `nBecause a reboot is required to register new patches, results are emailed following every reboot.  The system will reboot shortly." -ForegroundColor Magenta }
        $Script:Attach = $false
        #SendEmail   #--[ Disabled so that the emails only go out are after reboot or on svc error ]--
        Get-WUInstallMicrosoftUpdateAcceptAll -Confirm:$falseAutoReboot:$true
    }else{
        $Script:MessageBody = $Script:MessageBody + "-- $Svc Failed to stop -- ABORTING --"
        if ($Script:Console){Write-Host "`n-- $Svc Failed to stop -- ABORTING --`n" -ForegroundColor Red }
        SendEmail
    }
   
    #--[ Things to do if no reboot occurs after running update.  Comment items out if not applicable. ]--
       Sleep -Seconds 60  #--[ Wait to see if no reboot has occurred ]--
    #if ($Script:KillKaspersky){
    #    ServiceMgr "klnagent" "running" 
    #}else{
    #    $SvcState = "running"
    #}
   
    #--[ Rerun the script with status option.  Not needed if forcing a restart ]--
    #$ScriptBlock = [Scriptblock]::Create((Get-Content $Script:ScriptFullPath -Raw))
    #Invoke-Command -NoNewScope -ArgumentList '-status $true' -ScriptBlock $ScriptBlock -Verbose  #--[ Rerun this script and send results if no reboot occurred ]--
   
    #--[ Force a reboot with random delay if none occurred.  Scheduled task already randomizes so this is optional ]--
    #$RndArray = @(5,10,15,20,25,30)
    #$Rnd = (new-object System.Random)
    #$RndDelay = $Array[ $Rnd.Next( $Array.Count ) ]
    $RndDelay = 1
    Restart-Computer -Confirm $false -Delay $RndDelay
}
   
function ScheduledTask ($TaskName){
    if (!(Get-ScheduledTask | Where-Object {$_.TaskName -like $TaskName })){
        if ($Script:Console){Write-Host '- Creating new scheduled task "'$TaskName'"...' -ForegroundColor Cyan }
        $Script:MessageBody = $Script:MessageBody + '- Creating new scheduled task "'+$TaskName+'"...<br>'
        $Principal = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest
        if ($TaskName -eq "UpdateStatus"){   #--[ Creates status task with 5 minute delay that runs at startup ]--
            $Argument = '-WindowStyle Hidden –Noninteractive -noprofile -nologo -executionpolicy Bypass -Command "&{'+$Script:ScriptFullPath+' -status $true}"'
            $Action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $Argument
            $Trigger = New-ScheduledTaskTrigger -AtStartup -RandomDelay (New-TimeSpan -Minutes 5)
            $Task = New-ScheduledTask -Action $Action -Trigger $Trigger -Settings (New-ScheduledTaskSettingsSet)
            try{
                $Create = Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal
            }catch{
                $ErrorMessage = $_.Exception.Message
                $FailedItem = $_.Exception.ItemName
                $Script:MessageBody = $Script:MessageBody + '- Scheduled Task "'+$TaskName+'" failed to be created...  $ErrorMessage<br>'
                if ($Script:Console){Write-Host '- Scheduled Task '"$TaskName"' failed to be created...  $ErrorMessage' -ForegroundColor Red }
            }   
        }else{      #--[ Creates patch task with 90 minute random delay ]--
            $Argument = '-WindowStyle Hidden –Noninteractive -noprofile -nologo -executionpolicy Bypass -Command "&{'+$Script:ScriptFullPath+'}"'
            $Action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $Argument
            $Trigger = New-ScheduledTaskTrigger -Daily -RandomDelay (New-TimeSpan -Minutes 90) -At $Script:RunTime
            $Task = New-ScheduledTask -Action $Action -Trigger $Trigger -Settings (New-ScheduledTaskSettingsSet)
            try{
                $Create = Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -ErrorAction Stop
            }catch{
                $ErrorMessage = $_.Exception.Message
                $FailedItem = $_.Exception.ItemName
                $Script:MessageBody = $Script:MessageBody + '- Scheduled Task "'+$TaskName+'" failed to be created...  $ErrorMessage<br>'
                if ($Script:Console){Write-Host '- Scheduled Task "'$TaskName'" failed to be created...  $ErrorMessage' -ForegroundColor Red }
            }   
        }
    }else{
        if ($Script:Console){Write-Host '- Task "'$TaskName'" already exists...' -ForegroundColor Cyan }
        $Script:MessageBody = $Script:MessageBody + '- Task "'+$TaskName+'" already exists...<br>'
    }       
}   

function DeployScript{    #--[ Copies this script to all other systems noted in the config files. ]--
    if ($Script:Console){Write-Host '--[ Deploying Updated Script to Peer Hosts ]------------' -ForegroundColor Yellow}
    foreach ($Target in $Script:PeerList.Split(",")){
    if ($Script:Console){Write-Host `n'--[ Deploying to'($Target.ToUpper())' ]-------------------------' -ForegroundColor Cyan}
        if ($Target -ne $ThisComputer){
            if (Test-Path "\\$Target\c$\scripts\unattendedupdate.ps1"){
                if ($Script:Console){Write-Host "  -- Existing files found..." -ForegroundColor green
                    try{
                        Get-ChildItem -Path "\\$Target\c$\scripts\" | where{$_.Name -match "unattendedupdate.*"} | Remove-Item -Force:$true -Confirm:$false
                    }catch{
                        if ($Script:Console){Write-Host "  -- File delete on $Target FAILED..." -ForegroundColor Red}
                        if ($Script:Console){Write-Host "  -- Error Message = "$_.Exception.Message}
                        if ($Script:Console){Write-Host "  -- Error Item = "$_.Exception.ItemName}
                        break
                    }   
                }
        
                if (!(Test-Path "\\$Target\c$\scripts\unattendedupdate.ps1")){
                    if ($Script:Console){Write-Host "  -- Deletion validated.  Files no longer detected..." -ForegroundColor green}
                }else{
                    if ($Script:Console){Write-Host "  -- Deletion FAILED..." -ForegroundColor red}
                }
   
                try{
                    Copy-Item -Path $Script:ScriptFullPath -Destination "\\$Target\c$\scripts\" -Force -Confirm:$false
                    Copy-Item -Path ($PSScriptRoot+'\'+$Script:ScriptName.split('.')[0]+'.xml') -Destination "\\$Target\c$\scripts\" -Force -Confirm:$false
                }catch{
                    if ($Script:Console){Write-Host "  -- File copy to $Target FAILED..." -ForegroundColor Red}
                        if ($Script:Console){Write-Host "  -- Error Message = "$_.Exception.Message}
                    if ($Script:Console){Write-Host "  -- Error Item = "$_.Exception.ItemName}
                    break
                }

                if (Test-Path "\\$Target\c$\scripts\unattendedupdate.ps1"){
                    if ($Script:Console){Write-Host "  -- Verified PS1 copy to $Target..." -ForegroundColor Green}
                }
                if (Test-Path "\\$Target\c$\scripts\unattendedupdate.xml"){
                    if ($Script:Console){Write-Host "  -- Verified XML copy to $Target..." -ForegroundColor Green}
                }
            }
        }else{
            if ($Script:Console){Write-Host '  -- Bypassing local system'($Target.ToUpper()) -ForegroundColor Magenta}       
        }   
    }
    if ($Script:Console){Write-Host `n"--- COMPLETED ---" -ForegroundColor Red }
    break
}

#==[ End of Functions / Start of Main Process ]===============================================

#--[ Load the external config file ]--
LoadConfig

if ($Script:Deploy){DeployScript}

#--[ Add header to html output file ]--
$Script:MessageBody = @()
$Script:MessageBody += '
<style Type="text/css">
    table.myTable { border:5px solid black;border-collapse:collapse; }
    table.myTable td { border:2px solid black;padding:5px}
    table.myTable th { border:2px solid black;padding:5px;background: #949494 }
    table.bottomBorder { border-collapse:collapse; }
    table.bottomBorder td, table.bottomBorder th { border-bottom:1px dotted black;padding:5px; }
    tr.noBorder td {border: 0; }
</style>'

$Script:MessageBody +=
'<table class="myTable">
    <tr class="noBorder"><td colspan=5><center><h1>- ' + $Script:eMailSubject + ' -</h1></td></tr>
    <tr class="noBorder"><td colspan=5><center>The following report displays all recently installed patches on the system.</center></td></tr>
    <tr class="noBorder"><td colspan=5></tr>
    <tr class="noBorder"><td colspan=5>- script executed by' + $Script:UserContext.Name + '</tr>
    <tr class="noBorder"><td colspan=5>- script version    :  ' + $Script:ScriptVer + '</tr><br>
'

ScheduledTask "UpdateStatus"
ScheduledTask "UnattendedUpdate"

if ($Status){
    if ($Script:Console){Write-Host "- Collecting results..." -ForegroundColor Cyan }
    $Script:MessageBody += '<tr class="noBorder"><td colspan=5>- Collecting results....</tr><br>'
    GetResults
}elseif (($Script:Manual) -or ($Script:RunDays -Match $Today)){   #--[ Update routine is governed by the week days noted in the config file ]--
    if(Test-Path $PSScriptRoot\Results.csv){Remove-Item -path $PSScriptRoot\Results.csv -confirm:$false }
    if ($Script:Console){Write-Host "- Running update routine..." -ForegroundColor Cyan }
    $Script:MessageBody = $Script:MessageBody + "- Running update routine....<br>"
    PatchIt
}else{
    if ($Script:Console){Write-Host "`n-- Nothing scheduled for today --`n" -ForegroundColor Cyan }
}

if ($Script:Console){Write-Host "--- COMPLETED ---" -ForegroundColor Red }

<#
--[ Sample XML config file.  Should use the same name as this script and be in the same folder. ]--


<!-- Settings & configuration file -->
<Settings>
    <General>
        <ReportName>Patch Processing</ReportName>
        <PeerList>server1,server2,server3,server4</PeerList>
        <RunDays>Monday,Wednesday,Friday</RunDays>
        <RunTime>1am</RunTime>
    </General>
    <Email>
        <from>UnattendedUpdate@domain.com</from>
        <To>you@domain.com</To>
        <Debug>you@domain.com</Debug>
        <Subject>Automated Patch Processing</Subject>
        <HTML>$true</HTML>
        <SmtpServer>10.10.10.1</SmtpServer>
    </Email>
    <Credentials>
        <UserName>domain\serviceaccount</UserName>
        <Password>76492d1ws656ertg116743a5345MgB8AHIAegB2AHUTGYT6ghjYAZQAxAGIATgBaAHwAYwAzADQANgA0AGEAMAAwADQAZgB7812q87hH087hnA2AGQAZAAQBmAGQAOAA0ADAHwAYwAzADQANgAEANgBiADAANwBkADEANAA4AGQAZgA3ADIAYQAwADYAZAA3AGUAZgBkAGYAZAA=</Password>
        <Key>kdhCh7HCvLO+E6/AWnHbuTeJ7I78hnsdXN0IObie8mE=</Key>
    </Credentials>
</Settings>   

#>