Updatomatic.psm1

function Get-WSUSReportSummary {
    [CmdletBinding()]
    param(
        [string] $FilePath,
        [switch] $ShowHTML,
        [switch] $Online,
        [switch] $PassThru
    )

    Write-Verbose -Message "Getting WSUS configuration..."
    $Configuration = Get-MyWSUSConfiguration -Type 'Email', 'Subscription', 'ComputerTargetGroups', 'Configuration', 'DatabaseConfiguration' -WarningVariable Warnings
    if ($Warnings) {
        Write-Warning -Message "$($Warnings[0])"
        return
    }

    Write-Verbose -Message "Getting WSUS computer updates..."
    $Computers = Get-MyWSUScomputerUpdates -UpdateSummary -GroupByAllGroupNames

    $ComputersNotContacted = [ordered] @{}
    foreach ($Group in $Computers.Keys) {
        foreach ($Computer in $Computers[$Group]) {
            if ($Computer.LastSyncTimeDays -ge 3 -or $Computer.LastReportedTimeDays -ge 3) {
                if (-not $ComputersNotContacted[$Computer.Id]) {
                    $ComputersNotContacted[$Computer.Id] = $Computer
                }
            }
        }
    }

    Write-Verbose -Message "Getting WSUS computer summary..."
    $Status = Get-MyWSUSComputerSummary

    Write-Verbose -Message "Creating HTML report - $FilePath"
    New-HTML {
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLPanelStyle -BorderRadius 0px
        New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin

        New-HTMLHeader {
            New-HTMLSection -Invisible {
                New-HTMLSection {
                    New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue
                } -JustifyContent flex-start -Invisible
                New-HTMLSection {
                    New-HTMLText -Text "Updatomatic - $($Script:Updatomatic['Version'])" -Color Blue
                } -JustifyContent flex-end -Invisible
            }
            New-HTMLSection {
                New-HTMLChart {
                    New-ChartPie -Name 'Computers needing updates' -Value $Status.ComputerTargetsNeedingUpdatesCount
                    New-ChartPie -Name 'Computers with errors' -Value $Status.ComputerTargetsWithUpdateErrorsCount
                    New-ChartPie -Name 'Computers up to date' -Value $Status.ComputersUpToDateCount
                }
            }
        }

        New-HTMLTab -Name 'Configuration' -IconSolid toolbox {
            New-HTMLTabPanel {
                New-HTMLTab -Name 'Configuration' {
                    New-HTMLTable -DataTable $Configuration.Configuration -Filtering -ScrollX -ExcludeProperty 'UpdateServer', 'ServerId'
                }
                New-HTMLTab -Name 'Email' {
                    New-HTMLTable -DataTable $Configuration.Email -Filtering -ScrollX -ExcludeProperty 'UpdateServer'
                }
                New-HTMLTab -Name 'Subscription' {
                    New-HTMLTable -DataTable $Configuration.Subscription -Filtering -ScrollX -ExcludeProperty 'UpdateServer'
                }
                New-HTMLTab -Name 'Computer Target Groups' {
                    New-HTMLTable -DataTable $Configuration.ComputerTargetGroups -Filtering -ScrollX -ExcludeProperty 'UpdateServer'
                }
                New-HTMLTab -Name 'Database Configuration' {
                    New-HTMLTable -DataTable $Configuration.DatabaseConfiguration -Filtering -ScrollX -ExcludeProperty 'UpdateServer'
                }
            }
        }
        New-HTMLTab -Name 'Computers' -IconSolid desktop {
            New-HTMLTabPanel {
                New-HTMLTab -Name 'Not reporting' {
                    New-HTMLSection -Name 'Not reporting' {
                        New-HTMLTable -DataTable $ComputersNotContacted.Values -Filtering -ScrollX -ExcludeProperty 'Id', 'AllComputerUpdates' {
                            New-HTMLTableCondition -Name 'LastSyncTimeDays' -HighlightHeaders 'LastSyncTimeDays', 'LastSyncTime' -ComparisonType number -Operator gt -Value 10 -BackgroundColor CoralRed
                            New-HTMLTableCondition -Name 'LastSyncTimeDays' -HighlightHeaders 'LastSyncTimeDays', 'LastSyncTime' -ComparisonType number -Operator le -Value 10 -BackgroundColor Salmon
                            New-HTMLTableCondition -Name 'LastSyncTimeDays' -HighlightHeaders 'LastSyncTimeDays', 'LastSyncTime' -ComparisonType number -Operator le -Value 3 -BackgroundColor SpringGreen

                            New-HTMLTableCondition -Name 'LastReportedTimeDays' -HighlightHeaders 'LastReportedTimeDays', 'LastReportedTime' -ComparisonType number -Operator gt -Value 10 -BackgroundColor CoralRed
                            New-HTMLTableCondition -Name 'LastReportedTimeDays' -HighlightHeaders 'LastReportedTimeDays', 'LastReportedTime' -ComparisonType number -Operator le -Value 10 -BackgroundColor Salmon
                            New-HTMLTableCondition -Name 'LastReportedTimeDays' -HighlightHeaders 'LastReportedTimeDays', 'LastReportedTime' -ComparisonType number -Operator le -Value 3 -BackgroundColor SpringGreen

                            New-HTMLTableCondition -Name 'PendingReboot' -BackgroundColor SpringGreen -ComparisonType string -Operator eq -Value $false -FailBackgroundColor Salmon
                            New-HTMLTableCondition -Name 'UpdateUnknownCount' -BackgroundColor SpringGreen -ComparisonType number -Operator eq -Value 0 -FailBackgroundColor Salmon
                            New-HTMLTableCondition -Name 'UpdateNotInstalledCount' -BackgroundColor SpringGreen -ComparisonType number -Operator eq -Value 0 -FailBackgroundColor Salmon
                            New-HTMLTableCondition -Name 'UpdateInstalledPendingRebootCount' -BackgroundColor SpringGreen -ComparisonType number -Operator eq -Value 0 -FailBackgroundColor Salmon
                            New-HTMLTableCondition -Name 'UpdateFailedCount' -BackgroundColor SpringGreen -ComparisonType number -Operator eq -Value 0 -FailBackgroundColor Salmon
                        } -WarningAction SilentlyContinue
                    }
                }
                foreach ($Group in $Computers.Keys) {
                    New-HTMLTab -Name $Group {
                        New-HTMLSection -Name $Group {
                            New-HTMLTable -DataTable $Computers[$Group] -Filtering -ScrollX -ExcludeProperty 'Id', 'AllComputerUpdates' {
                                New-HTMLTableCondition -Name 'LastSyncTimeDays' -HighlightHeaders 'LastSyncTimeDays', 'LastSyncTime' -ComparisonType number -Operator gt -Value 10 -BackgroundColor CoralRed
                                New-HTMLTableCondition -Name 'LastSyncTimeDays' -HighlightHeaders 'LastSyncTimeDays', 'LastSyncTime' -ComparisonType number -Operator le -Value 10 -BackgroundColor Salmon
                                New-HTMLTableCondition -Name 'LastSyncTimeDays' -HighlightHeaders 'LastSyncTimeDays', 'LastSyncTime' -ComparisonType number -Operator le -Value 3 -BackgroundColor SpringGreen

                                New-HTMLTableCondition -Name 'LastReportedTimeDays' -HighlightHeaders 'LastReportedTimeDays', 'LastReportedTime' -ComparisonType number -Operator gt -Value 10 -BackgroundColor CoralRed
                                New-HTMLTableCondition -Name 'LastReportedTimeDays' -HighlightHeaders 'LastReportedTimeDays', 'LastReportedTime' -ComparisonType number -Operator le -Value 10 -BackgroundColor Salmon
                                New-HTMLTableCondition -Name 'LastReportedTimeDays' -HighlightHeaders 'LastReportedTimeDays', 'LastReportedTime' -ComparisonType number -Operator le -Value 3 -BackgroundColor SpringGreen

                                New-HTMLTableCondition -Name 'PendingReboot' -BackgroundColor SpringGreen -ComparisonType string -Operator eq -Value $false -FailBackgroundColor Salmon
                                New-HTMLTableCondition -Name 'UpdateUnknownCount' -BackgroundColor SpringGreen -ComparisonType number -Operator eq -Value 0 -FailBackgroundColor Salmon
                                New-HTMLTableCondition -Name 'UpdateNotInstalledCount' -BackgroundColor SpringGreen -ComparisonType number -Operator eq -Value 0 -FailBackgroundColor Salmon
                                New-HTMLTableCondition -Name 'UpdateInstalledPendingRebootCount' -BackgroundColor SpringGreen -ComparisonType number -Operator eq -Value 0 -FailBackgroundColor Salmon
                                New-HTMLTableCondition -Name 'UpdateFailedCount' -BackgroundColor SpringGreen -ComparisonType number -Operator eq -Value 0 -FailBackgroundColor Salmon
                            } -WarningAction SilentlyContinue
                        }
                    }
                }
            }
        }
        New-HTMLTab -Name 'Computer Updates' -IconSolid cloud {
            New-HTMLTabPanel {
                foreach ($Group in $Computers.Keys) {
                    New-HTMLTab -Name $Group {
                        New-HTMLSection -Name $Group {
                            $Updates = $Computers[$Group].AllComputerUpdates | ForEach-Object { if ($_) {
                                    $_ 
                                } }
                            New-HTMLTable -DataTable $Updates -Filtering -ScrollX {
                                New-HTMLTableCondition -Name 'ApprovalStatus' -BackgroundColor SpringGreen -ComparisonType string -Operator eq -Value "Install" -FailBackgroundColor LightYellow
                                New-HTMLTableCondition -Name 'UpdateInstallationState' -BackgroundColor Salmon -ComparisonType string -Operator eq -Value 'Unknown'
                                New-HTMLTableCondition -Name 'UpdateInstallationState' -BackgroundColor Crimson -ComparisonType string -Operator eq -Value 'NotInstalled'
                                New-HTMLTableCondition -Name 'UpdateInstallationState' -BackgroundColor MoonYellow -ComparisonType string -Operator eq -Value 'InstalledPendingReboot'
                                New-HTMLTableCondition -Name 'UpdateInstallationState' -BackgroundColor SpringGreen -ComparisonType string -Operator eq -Value 'Installed'
                                New-HTMLTableCondition -Name 'UpdateInstallationState' -BackgroundColor CornflowerBlue -ComparisonType string -Operator eq -Value 'Downloaded'

                                New-HTMLTableCondition -Name 'LastReportedTimeDays' -HighlightHeaders 'LastReportedTimeDays' -ComparisonType number -Operator gt -Value 10 -BackgroundColor CoralRed
                                New-HTMLTableCondition -Name 'LastReportedTimeDays' -HighlightHeaders 'LastReportedTimeDays' -ComparisonType number -Operator le -Value 10 -BackgroundColor Salmon
                                New-HTMLTableCondition -Name 'LastReportedTimeDays' -HighlightHeaders 'LastReportedTimeDays' -ComparisonType number -Operator le -Value 3 -BackgroundColor SpringGreen
                            } -WarningAction SilentlyContinue
                        }
                    }
                }
            }
        }
    } -FilePath $FilePath -ShowHTML:$ShowHTML -Online:$Online

    if ($PassThru) {
        [ordered] @{
            Configuration = $Configuration
            Computers     = $Computers
        }
    }
}
function Get-WSUSUpdatesForComputer {
    [CmdletBinding()]
    param(
        [Microsoft.UpdateServices.Internal.BaseApi.ComputerTarget] $ComputerTarget,
        [int] $LastReportedTimeDays = -1,
        [DateTime] $Today = (Get-Date)
    )

    if (-not $Script:Updatomatic.CachedUpdatesInformation) {
        Write-Verbose -Message "Getting WSUS available updates... (cache only), as no cached updates information is available."
        Get-MyWSUSUpdateState -CacheOnly
    }
    Write-Verbose -Message "Getting updates status/details for computer $($ComputerTarget.FullDomainName)"
    $AllComputerUpdates = $ComputerTarget.GetUpdateInstallationInfoPerUpdate()
    foreach ($Update in $AllComputerUpdates) {
        if ($Update.UpdateInstallationState -ne [Microsoft.UpdateServices.Administration.UpdateInstallationState]::NotApplicable) {

            $UpdateInformation = $Script:Updatomatic.CachedUpdatesInformation[$Update.UpdateId.Guid]

            [PSCustomObject]@{
                KBNumber                = $UpdateInformation.KnowledgebaseArticles -join ", "
                ApprovalStatus          = $Update.UpdateApprovalAction
                UpdateInstallationState = $Update.UpdateInstallationState
                ArrivalDateDays         = [math]::Round(($Today - $UpdateInformation.ArrivalDate).TotalDays, 0)
                LastReportedTimeDays    = $LastReportedTimeDays
                UpdateType              = $UpdateInformation.UpdateType
                ProductTitles           = $UpdateInformation.ProductTitles 
                UpdateTitle             = $UpdateInformation.Title
                IsLatestRevision        = $UpdateInformation.IsLatestRevision

                ArrivalDate             = $UpdateInformation.ArrivalDate

                ComputerName            = $ComputerTarget.FullDomainName
            }
        }
    }
}

function Connect-MyWSUS {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $ServerName,
        [int] $Port = 8530,
        [switch] $UseSsl,
        [switch] $DoNotSuppress
    )
    try {
        $WSUSServer = Get-WsusServer -Name $ServerName -PortNumber $Port -UseSsl:$UseSsl -ErrorAction Stop
    } catch {
        if ($ErrorActionPreference -eq 'Stop') {
            throw
        } else {
            Write-Warning -Message "Connect-MyWSUS - Couldn't connect to WSUS server: $ServerName. Error: $($_.Exception.Message)"
            return
        }
    }
    $Script:Updatomatic = @{
        Version    = "0.6.0"
        WSUSServer = $WSUSServer
    }
    if ($DoNotSuppress) {
        $Script:Updatomatic
    }
}
function Get-MyWSUSComputerClient {
    [cmdletbinding(DefaultParameterSetName = 'AllComputers')]
    param(
        [Parameter(Position = 0, ParameterSetName = 'Computer')]
        [string[]]$ComputerName,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [Microsoft.UpdateServices.Administration.UpdateInstallationStates]$IncludedInstallationState,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [Microsoft.UpdateServices.Administration.UpdateInstallationStates]$ExcludedInstallState,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [Microsoft.UpdateServices.Internal.BaseApi.ComputerTargetGroup[]]$ComputerTargetGroups,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [DateTime]$FromLastStatusTime,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [DateTime]$ToLastStatusTime,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [DateTime]$FromLastSyncTime,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [DateTime]$ToLastSyncTime,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [string]$OSFamily,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [switch]$IncludeSubGroups,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [switch]$IncludeDownstreamComputerTargets
    )

    if (-not $Script:Updatomatic) {
        Write-Warning -Message "Get-MyWSUSComputerClient - Updatomatic is not connected to a WSUS server. Use Connect-MyWSUS to connect."
        return
    }

    $WSUSServer = $Script:Updatomatic.WSUSServer

    if ($PSCmdlet.ParameterSetName -eq 'ComputerScope') {
        $ComputerScope = [Microsoft.UpdateServices.Administration.ComputerTargetScope]::new()
        If ($PSBoundParameters['IncludedInstallationState']) {
            $ComputerScope.IncludedInstallationStates = $IncludedInstallationState
        }
        If ($PSBoundParameters['ExcludedInstallState']) {
            $ComputerScope.ExcludedInstallationStates = $ExcludedInstallState
        }
        If ($PSBoundParameters['FromLastStatusTime']) {
            $ComputerScope.FromLastReportedStatusTime = $FromLastStatusTime
        }
        If ($PSBoundParameters['ToLastStatusTime']) {
            $ComputerScope.ToLastReportedStatusTime = $ToLastStatusTime
        }
        If ($PSBoundParameters['FromLastSyncTime']) {
            $ComputerScope.FromLastSyncTime = $FromLastSyncTime
        }
        If ($PSBoundParameters['ToLastSyncTime']) {
            $ComputerScope.ToLastSyncTime = $ToLastSyncTime
        }
        If ($PSBoundParameters['IncludeSubGroups']) {
            $ComputerScope.IncludeSubgroups = $IncludeSubGroups
        }
        If ($PSBoundParameters['OSFamily']) {
            $ComputerScope.OSFamily = $OSFamily
        }
        If ($PSBoundParameters['IncludeDownstreamComputerTargets']) {
            $ComputerScope.IncludeDownstreamComputerTargets = $IncludeDownstreamComputerTargets
        }
        If ($PSBoundParameters['ComputerTargetGroups']) {
            $null = $ComputerScope.ComputerTargetGroups.AddRange($ComputerTargetGroups)
        }
    }

    if ($PSCmdlet.ParameterSetName -eq 'AllComputers') {
        Write-Verbose "Get-MyWSUSComputerClient - Gathering computers from all computer targets"
        $WSUSServer.GetComputerTargets()
    } elseif ($PSCmdlet.ParameterSetName -eq 'Computer') {
        $Count = 0
        ForEach ($Computer in $ComputerName) {
            $Count++
            Write-Verbose "Get-MyWSUSComputerClient - [$Count/$($ComputerName.Count)]Retrieve computer in WSUS"
            Try {
                $WSUSServer.SearchComputerTargets($Computer)
            } Catch {
                Write-Warning -Message "Get-MyWSUSComputerClient - Unable to retrieve $Computer from WSUS database."
            }
        }
    } elseif ($PSCmdlet.ParameterSetName -eq 'ComputerScope') {
        Write-Verbose -Message "Get-MyWSUSComputerClient - Gathering computers from computer target scope"
        $WSUSServer.GetComputerTargets($ComputerScope)
    }
}

function Get-MyWSUSComputerSummary {
    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName = 'ComputerScope')]

        [Microsoft.UpdateServices.Administration.UpdateInstallationStates]$IncludedInstallationState,
        [Parameter(ParameterSetName = 'ComputerScope')]

        [Microsoft.UpdateServices.Administration.UpdateInstallationStates]$ExcludedInstallState,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [string[]]$ComputerTargetGroupsName,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [string[]]$ComputerTargetGroupsId,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [DateTime]$FromLastStatusTime,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [DateTime]$ToLastStatusTime,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [DateTime]$FromLastSyncTime,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [DateTime]$ToLastSyncTime,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [string]$OSFamily,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [switch]$IncludeSubGroups,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [switch]$IncludeDownstreamComputerTargets,

        [Parameter(ParameterSetName = 'ComputerScope')]
        [Microsoft.UpdateServices.Administration.UpdateSources] $UpdateSources = [Microsoft.UpdateServices.Administration.UpdateSources]::All
    )

    if (-not $Script:Updatomatic) {
        Write-Warning -Message "Get-MyWSUSComputerStatus - Updatomatic is not connected to a WSUS server. Use Connect-MyWSUS to connect."
        return
    }

    $WSUSServer = $Script:Updatomatic.WSUSServer

    if ($ComputerTargetGroupsName -or $ComputerTargetGroupsId) {
        $ComputerTargetGroups = Get-MyWSUSComputerTargetGroups -ComputerTargetGroupsName $ComputerTargetGroupsName -ComputerTargetGroupsId $ComputerTargetGroupsId
        if ($ComputerTargetGroups.Count -ne ($ComputerTargetGroupsName.Count + $ComputerTargetGroupsId.Count)) {
            Write-Warning -Message "Get-MyWSUSComputerStatus - Requested amount of ComputerTargetGroups doesn't match. Terminating early."
            return
        }
    } else {
        $ComputerTargetGroups = $null
    }

    if ($PSCmdlet.ParameterSetName -eq 'ComputerScope') {
        $ComputerScope = [Microsoft.UpdateServices.Administration.ComputerTargetScope]::new()
        If ($PSBoundParameters['IncludedInstallationState']) {
            $ComputerScope.IncludedInstallationStates = $IncludedInstallationState
        }
        If ($PSBoundParameters['ExcludedInstallState']) {
            $ComputerScope.ExcludedInstallationStates = $ExcludedInstallState
        }
        If ($PSBoundParameters['FromLastStatusTime']) {
            $ComputerScope.FromLastReportedStatusTime = $FromLastStatusTime
        }
        If ($PSBoundParameters['ToLastStatusTime']) {
            $ComputerScope.ToLastReportedStatusTime = $ToLastStatusTime
        }
        If ($PSBoundParameters['FromLastSyncTime']) {
            $ComputerScope.FromLastSyncTime = $FromLastSyncTime
        }
        If ($PSBoundParameters['ToLastSyncTime']) {
            $ComputerScope.ToLastSyncTime = $ToLastSyncTime
        }
        If ($PSBoundParameters['IncludeSubGroups']) {
            $ComputerScope.IncludeSubgroups = $IncludeSubGroups
        }
        If ($PSBoundParameters['OSFamily']) {
            $ComputerScope.OSFamily = $OSFamily
        }
        If ($PSBoundParameters['IncludeDownstreamComputerTargets']) {
            $ComputerScope.IncludeDownstreamComputerTargets = $IncludeDownstreamComputerTargets
        }
        If ($ComputerTargetGroups) {
            $null = $ComputerScope.ComputerTargetGroups.AddRange($ComputerTargetGroups)
        }
    }

    $WSUSServer.GetComputerStatus($ComputerScope, $UpdateSources)
}
function Get-MyWSUSComputerTargetGroups {
    [CmdletBinding()]
    param(
        [alias('Name')][string[]]$ComputerTargetGroupsName,
        [alias('Id')][string[]]$ComputerTargetGroupsId
    )

    if (-not $Script:Updatomatic) {
        Write-Warning -Message "Get-MyWSUSComputerTargetGroups - Updatomatic is not connected to a WSUS server. Use Connect-MyWSUS to connect."
        return
    }

    $WSUSServer = $Script:Updatomatic.WSUSServer

    $Configuration = $WSUSServer.GetConfiguration()
    $AllComputerTargetGroups = [Microsoft.UpdateServices.Internal.BaseApi.ComputerTargetGroup]::GetAll($Configuration.UpdateServer)

    if ($ComputerTargetGroupsName -or $ComputerTargetGroupsId) {
        $Configuration = $WSUSServer.GetConfiguration()
        $AllComputerTargetGroups = [Microsoft.UpdateServices.Internal.BaseApi.ComputerTargetGroup]::GetAll($Configuration.UpdateServer)
        if ($ComputerTargetGroupsName) {
            $Names = foreach ($Name in $ComputerTargetGroupsName) {
                foreach ($TargetGroup in $AllComputerTargetGroups) {
                    if ($TargetGroup.Name -eq $Name) {
                        Write-Verbose -Message "Found ComputerTargetGroup: $($TargetGroup.Name)"
                        $TargetGroup
                    }
                }
            }
            if ($Names) {
                $Names
            } else {
                Write-Warning -Message "ComputerTargetGroup not found: $Name"
            }
        }
        if ($ComputerTargetGroupsId) {
            $Ids = foreach ($Id in $ComputerTargetGroupsId) {
                foreach ($TargetGroup in $AllComputerTargetGroups) {
                    if ($TargetGroup.Id -eq $Id) {
                        Write-Verbose -Message "Found ComputerTargetGroup: $($TargetGroup.Name)"
                        $TargetGroup
                    }
                }
            }
            if ($Ids) {
                $Ids
            } else {
                Write-Warning -Message "ComputerTargetGroup not found: $Id"
            }
        }
    } else {
        $AllComputerTargetGroups
    }
}
function Get-MyWSUScomputerUpdates {
    [CmdletBinding()]
    param(
        [Microsoft.UpdateServices.Administration.UpdateApprovalActions] $UpdateScope,
        [switch] $UpdateSummary,
        [switch] $GroupByGroupName,
        [switch] $GroupByAllGroupNames
    )

    if (-not $Script:Updatomatic) {
        Write-Warning -Message "Get-MyWSUScomputerUpdates - Updatomatic is not connected to a WSUS server. Use Connect-MyWSUS to connect."
        return
    }
    $Today = [datetime]::Now
    $Grouping = [ordered] @{}

    Get-MyWSUSUpdateState -CacheOnly

    $WSUSServer = $Script:Updatomatic.WSUSServer

    $Scope = [Microsoft.UpdateServices.Administration.ComputerTargetScope]::new()

    $Count = 0
    $Computers = $WSUSServer.GetComputerTargets($Scope)
    foreach ($Computer in $Computers) {
        $Count++
        Write-Verbose -Message "Processing computer [$Count/$($Computers.Count)] $($Computer.FullDomainName)]"

        $LastReportedTimeDays = [math]::Round(($Today - $Computer.LastReportedStatusTime).TotalDays, 0)

        $AllComputerUpdates = Get-WsusUpdatesForComputer -ComputerTarget $Computer -LastReportedTimeDays $LastReportedTimeDays -Today $Today

        $AllTargetGroups = $Computer.GetComputerTargetGroups() | ForEach-Object { $_.Name }

        $FullDomainNameParts = $Computer.FullDomainName -split '\.'
        $HostName = $FullDomainNameParts[0]
        $Domain = ($FullDomainNameParts[1..($FullDomainNameParts.Length - 1)] -join '.')

        $Output = [ordered] @{
            Id                   = $Computer.Id                        
            HostName             = $HostName
            Domain               = $Domain
            FullDomainName       = $Computer.FullDomainName            
            Groups               = $AllTargetGroups
            PendingReboot        = $null
            LastSyncTimeDays     = [math]::Round(($Today - $Computer.LastSyncTime).TotalDays, 0)
            LastReportedTimeDays = $LastReportedTimeDays
        }

        $EndData = [ordered] @{
            IPAddress          = $Computer.IPAddress                 
            Make               = $Computer.Make                      
            Model              = $Computer.Model                     

            ClientVersion      = $Computer.ClientVersion             

            OS                 = $Computer.OSDescription             
            ComputerRole       = $Computer.ComputerRole              
            LastSyncTime       = $Computer.LastSyncTime              
            LastSyncResult     = $Computer.LastSyncResult            
            LastReportedTime   = $Computer.LastReportedStatusTime    

            RequestedGroup     = $Computer.RequestedTargetGroupName  
            AllComputerUpdates = $AllComputerUpdates
        }

        if ($UpdateSummary) {
            $UpdateInstallationSummary = Get-MyWSUSSummaryUpdateInstallation -ComputerTarget $Computer -Today $Today
            if ($UpdateInstallationSummary.UpdateInstalledPendingRebootCount -gt 0) {
                $Output.PendingReboot = $true
            } else {
                $Output.PendingReboot = $false
            }
            $Object = [PSCustomObject] ($Output + $UpdateInstallationSummary + $EndData)
        } else {
            $Object = [PSCustomObject] ($Output + $EndData)
        }
        if ($GroupByAllGroupNames) {
            foreach ($Group in $Output.Groups) {
                if (-not $Grouping[$Group]) {
                    $Grouping[$Group] = [System.Collections.Generic.List[PSCustomObject]]::new()
                }
                $Grouping[$Group].Add($Object)
            }
        } elseif ($GroupByGroupName) {
            if (-not $Grouping[$EndData.RequestedGroup]) {
                $Grouping[$EndData.RequestedGroup] = [System.Collections.Generic.List[PSCustomObject]]::new()
            }
            $Grouping[$EndData.RequestedGroup].Add($Object)
        } else {
            $Object
        }
    }
    if ($GroupByGroupName -or $GroupByAllGroupNames) {
        $Grouping
    }
}

function Get-MyWSUSConfiguration {
    [CmdletBinding()]
    param(
        [ValidateSet('Email', 'Subscription', 'ComputerTargetGroups', 'Configuration', 'DatabaseConfiguration')]
        [string[]] $Type = 'Configuration'
    )

    if (-not $Script:Updatomatic) {
        Write-Warning -Message "Get-MyWSUSConfiguration - Updatomatic is not connected to a WSUS server. Use Connect-MyWSUS to connect."
        return
    }

    $WSUSServer = $Script:Updatomatic.WSUSServer

    $OutputConfigurations = [ordered] @{}

    foreach ($T in $Type) {
        switch ($T) {
            'Email' {
                $OutputConfigurations['Email'] = $WSUSServer.GetEmailNotificationConfiguration()
            }
            'Subscription' {
                $OutputConfigurations['Subscription'] = $WSUSServer.GetSubscription()
            }
            'ComputerTargetGroups' {
                $OutputConfigurations['ComputerTargetGroups'] = $WSUSServer.GetComputerTargetGroups()
            }
            'Configuration' {
                $OutputConfigurations['Configuration'] = $WSUSServer.GetConfiguration()
            }
            'DatabaseConfiguration' {
                $OutputConfigurations['DatabaseConfiguration'] = $WSUSServer.GetDatabaseConfiguration()
            }
            default {
                Write-Warning -Message "Get-MyWSUSConfiguration - Unknown type: $T"
            }
        }
    }
    if ($Type.Count -eq 1) {
        $OutputConfigurations[$Type[0]]
    } else {
        $OutputConfigurations
    }
}
function Get-MyWSUSStatus {
    [CmdletBinding()]
    param(

    )

    if (-not $Script:Updatomatic) {
        Write-Warning -Message "Get-MyWSUSStatus - Updatomatic is not connected to a WSUS server. Use Connect-MyWSUS to connect."
        return
    }

    $WSUSServer = $Script:Updatomatic.WSUSServer
    $WSUSServer.GetStatus()
}
function Get-MyWSUSSummaryUpdateInstallation {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][Microsoft.UpdateServices.Internal.BaseApi.ComputerTarget] $ComputerTarget,
        [DateTime] $Today = (Get-Date)
    )

    $Data = $ComputerTarget.GetUpdateInstallationSummary()

    [ordered] @{
        UpdateUnknownCount                = $Data.UnknownCount
        UpdateNotInstalledCount           = $Data.NotInstalledCount
        UpdateDownloadedCount             = $Data.DownloadedCount
        UpdateInstalledCount              = $Data.InstalledCount
        UpdateInstalledPendingRebootCount = $Data.InstalledPendingRebootCount
        UpdateFailedCount                 = $Data.FailedCount

        UpdateNotApplicableCount          = $Data.NotApplicableCount
        UpdateLastUpdated                 = $Data.LastUpdated
        UpdateLastUpdatedDays             = [math]::Round(($Today - $Data.LastUpdated).TotalDays, 0)
    }
}

function Get-MyWSUSUpdateState {
    [CmdletBinding()]
    param(
        [Microsoft.UpdateServices.Administration.UpdateApprovalActions] $UpdateScope,
        [switch] $CacheOnly
    )

    if (-not $Script:Updatomatic) {
        Write-Warning -Message "Get-MyWSUSUpdateState: Updatomatic is not connected to a WSUS server. Use Connect-MyWSUS to connect."
        return
    }

    if ($CacheOnly) {
        Write-Verbose -Message "Getting WSUS available updates... (cache only)"
    } else {
        Write-Verbose -Message "Getting WSUS available updates..."
    }

    $WSUSServer = $Script:Updatomatic.WSUSServer

    $CachedUpdatesInformation = [ordered] @{}

    if ($UpdateScope) {
        $UpdateScope = [Microsoft.UpdateServices.Administration.UpdateScope]::new()
        $UpdateScope.UpdateApprovalActions = $UpdateScope
        $ListUpdates = $WSUSServer.GetUpdates($UpdateScope)
    } else {
        $ListUpdates = $WSUSServer.GetUpdates()
    }

    foreach ($Update in $ListUpdates) {
        $CachedUpdatesInformation[$update.Id.UpdateId.Guid] = $Update
        if (-not $CacheOnly) {
            $Update
        }
    }

    $Script:Updatomatic.CachedUpdatesInformation = $CachedUpdatesInformation
}
function Show-MyWSUS {
    [CmdletBinding()]
    param(
        [string] $FilePath,
        [switch] $ShowHTML,
        [switch] $Online,
        [Parameter(Mandatory)][ValidateSet(
            'ComputersSummary'
        )][string] $Reports,
        [switch] $PassThru
    )

    if ($Reports -eq 'ComputersSummary') {
        Get-WSUSReportSummary -FilePath $FilePath -ShowHTML:$ShowHTML -Online:$Online -PassThru:$PassThru
    } else {
        Write-Warning -Message "Show-MyWSUS - Unknown report: $Reports"
    }
}

Export-ModuleMember -Function @('Connect-MyWSUS', 'Get-MyWSUSComputerClient', 'Get-MyWSUSComputerSummary', 'Get-MyWSUSComputerTargetGroups', 'Get-MyWSUScomputerUpdates', 'Get-MyWSUSConfiguration', 'Get-MyWSUSStatus', 'Get-MyWSUSSummaryUpdateInstallation', 'Get-MyWSUSUpdateState', 'Show-MyWSUS') -Alias @()
# SIG # Begin signature block
# MIItqwYJKoZIhvcNAQcCoIItnDCCLZgCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB2PSHESyEvB6Lq
# rvlO/FcFwpHlVFh2045+f/Hbq6KYkKCCJq4wggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw
# aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK
# EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm
# dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu
# d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD
# eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1
# XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld
# QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS
# YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm
# M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT
# QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx
# fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
# VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq
# hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4
# XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ
# aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg
# X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk
# apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL
# FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy
# 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u
# KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54
# zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8
# 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8
# aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ
# CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw
# MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k
# jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9
# NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9
# URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY
# E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS
# 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa
# wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w
# c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR
# Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2
# 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK
# ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC
# AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2
# O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH
# BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6
# mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/
# SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY
# gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9
# kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ
# 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew
# Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm
# Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA
# SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr
# y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR
# ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu
# v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGvDCCBKSgAwIBAgIQC65mvFq6f5WHxvnp
# BOMzBDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5
# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAwMDAwMFoXDTM1MTEy
# NTIzNTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSAwHgYD
# VQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQAD
# ggIPADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjEiDtqmeOlwf0KMCBD
# Er4IxHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOcRe8+CEJp+3R2O8oo
# 76EO7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/GLoUb35SfWHh43rO
# H3bpLEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0ChaV76Nhnj37DEYTX9R
# eNZ8hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8UuKGn9966fR5X6kgX
# j3o5WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHwSJ+QQRZ1fisD8UTV
# DSupWJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4EfvFrpVNnes4c16J
# idj5XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzIXp4P0wXkgNs+CO/C
# acBqU0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3JyidxW48jwBqIJqImd93N
# Rxvd1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizchNULpUEoA6Vva7b1X
# CB+1rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJcv6dQ4aEKOX5AgMB
# AAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUB
# Af8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s
# BwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFJ9X
# LAN3DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwz
# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1l
# U3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhho
# dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNl
# cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZU
# aW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAD2tHh92mVvjOIQS
# R9lDkfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq3igpwrPvBmZdrlWB
# b0HvqT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcHzBMutB6HzeledbDC
# zFzUy34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTVOoJ4eTq7gj9UFAL1
# UruJKlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4Hv5swO+aAXxWUm3Wp
# ByXtgVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgtd7/fvWTlCs30VAGE
# sshJmLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaidRJXrI+UzB6vAlk/8
# a1u7cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhdmm4bhYsVA6G2WgNF
# YagLDBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dHPoWrUhftNpFC5H7Q
# EY7MhKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDiCLg4D+TPVgKx2EgE
# deoHNHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7zcEO1xwcdcqJsyz/J
# ceENc2Sg8h3KeFUCS7tpFk7CrDqkMIIHXzCCBUegAwIBAgIQB8JSdCgUotar/iTq
# F+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAwMDAwMFoX
# DTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1pa2/FgsOz
# dzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYDVQQDDBhQ
# cnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
# ggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmVOrRBVRQA
# 8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVEh0C/Daeh
# vxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNdGVXRYOLn
# 47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0235CN4Rr
# W+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuAo3+jVB8w
# iUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw8/FNzGNP
# lAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP0ib98XLf
# QpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxiW4oHYO28
# eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFKRqwvSSr4
# fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKABGoIqSW0
# 5nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQIDAQABo4IC
# AzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYE
# FHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
# BggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEz
# ODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j
# cmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3
# dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcw
# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8v
# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmlu
# Z1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB
# CwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50ZHzoWs6E
# BlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa1W47YSrc
# 5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2CbE3JroJ
# f2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0djvQSx51
# 0MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N9E8hUVev
# xALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgizpwBasrx
# h6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38wwtaJ3KY
# D0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Yn8kQMB6/
# Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Zn3exUAKq
# G+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe6nB6bSYH
# v8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGCBlMwggZP
# AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEw
# PwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2
# IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgBZQMEAgEF
# AKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgor
# BgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3
# DQEJBDEiBCBrWkQdVr233atj7I6P9V9buz7OdJ0Td00AoqQZVi8OFDANBgkqhkiG
# 9w0BAQEFAASCAgAh/uUKIlVhIbQYQ30l9TRfOjK3+JnnFbJAPmwPuLB791afg1hj
# YKbCfv+p+WEFBlE7eB7Zu6A9H3XRQBGXSQwvemFNpHqxFA3nRjyvW9kOREWBkvph
# IxO44W8oJnOB/67jR5zwsjzw0kfTIc8YETiOzrmDuaUpDnDaMrT0qmYWXvcz3roC
# Ygh1UWdrVUmZE0BHnRioPM8dKffJ/jEegKOeGyMAzfUjclDAl5++FLRvSrAel6U0
# vXGEIaiOnJ4qoKOQg8Ix3+sX/umCXvTFQYkPQ4TEeDy6TWmhLVnkvrC/I3laDqY8
# MemM7FzpldJqysC1x5mWbu58LgY7Ru32dnVooC8fbJj9dFl89ZfS4cTAleqNen6W
# dUl+hC+xMu+TW2SQH5BiDPOYt5eK+9V7T0N3+wbbpSPgTRsRaXzZsaoaFfjoxims
# h5rF06ysTqZEGao2/7633rMqW543noJy/z3xp4Bb1JcKPWrVSx+YLTjlKEvwgagi
# BKsm4vBT06teizbPt1BXOR3KXjKkgGuXt5vRLgKCPOGZKfP74/lVw92KVZJrEygg
# a5kmDgXZ9soSFY+xuZOnCgvPQx25/K6Tao7zwhaAS3V28Yy5IeMWTdtxeQ9auk7l
# kgzVMl8GJrPHqX5SiLLnLNLlrqsApW+kvLBXSsPq8jRJ6X2Pu+Qn1OWEbqGCAyAw
# ggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYD
# VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH
# NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAuuZrxaun+Vh8b56QTj
# MwQwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNDExMjExODM3MTZaMC8GCSqGSIb3DQEJBDEiBCCx2TqD
# gLUvLe27kl7xEOzRwWBIt1Ctl+Dh8Z9zZLd4TTANBgkqhkiG9w0BAQEFAASCAgAF
# Ox99qDBqx0OVh/bP5hZRL01exX09JNcwc4F0XCwm40p6UKFTp9RXnLTUsyZoxUPE
# n9SFhHcmnZfaYJPruXiwwzdy+6owMErZByf/MTUlfxfc8dsfmk36+GFJ3MuE3mDE
# Fh9G97DUBtLNvcMpb6CjdlLg/nAKuBRSH/IGwgP+rwLeAUZCGJEb/h5SerpDpjp+
# QyZRFtyo1TMp+CuhQm6ZHBS8f3f3StQJPWCQ3K1W/PY0gRMWBqpgGWNfDFNcfVC+
# tfRMSvQqlrcDWQgDlOAN2llzpsRNEdJIycEwMDsVJnCQujdsBeiKHKlXHaygAiso
# GFb06hQuJ3KzhjceAMxNce2xAvLvZ3MDqYwu4/yVdVlOXjKRHaaEpts3LOhGTyvy
# Y+qQESj4eppRKUh6WRpBHHqqDMyhaIG/b7v1mksqeieXCBEO7useX6Lo5WZY3MrJ
# yMiWGrIr/JyZ/3oRWz7yvZzDO8td1bhl/Ise6JoiqMTJ79Rbh/R+V+qtaEJ9vzwz
# zYONHvCGTorXRb0GgFQrLciUs40WvGAA6OnoqpUbxh/nxdsfOWgz6rNTNgvLM/Sp
# 11oeJZJR3YlC0AJhNPqQ/DbnNopCERnUWs8GRU1VJKa32pc6JBHsZh/6292uBrSv
# qyGTIUPzWaehIxx/t62rjZvci8HnR6+4TPsBzxYLlg==
# SIG # End signature block