jasimancas
10/23/2017 - 11:40 AM

Reporte de Actualizaciones WSUS

Cambiar el nombre del servidor ($WSUSServer)

<#
    WSUS Report
    
    ** Requires WSUS Administrator Console Installed or UpdateServices Module available **        
    
    TO DO:
        - SUSDB Size
        - Computers in Active Directory but not in WSUS (OPTIONAL)
#>

#region User Specified WSUS Information
$WSUSServer = 'XXXX'

#Accepted values are "80","443","8530" and "8531"
$Port = 8530 
$UseSSL = $False

#Specify when a computer is considered stale
$DaysComputerStale = 50 

#Send email of report
[bool]$SendEmail = $FALSE
#Display HTML file
[bool]$ShowFile = $FALSE
#endregion User Specified WSUS Information

#region User Specified Email Information
$EmailParams = @{
    To = 'user@domain.local'
    From = 'WSUSReport@domain.local'    
    Subject = "$WSUSServer WSUS Report"
    SMTPServer = 'exchange.domain.local'
    BodyAsHtml = $True
}
#endregion User Specified Email Information

#region Helper Functions
Function Set-AlternatingCSSClass {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True,ValueFromPipeline=$True)]
        [string]$HTMLFragment,
        [Parameter(Mandatory=$True)]
        [string]$CSSEvenClass,
        [Parameter(Mandatory=$True)]
        [string]$CssOddClass
    )
    [xml]$xml = $HTMLFragment
    $table = $xml.SelectSingleNode('table')
    $classname = $CSSOddClass
    foreach ($tr in $table.tr) {
        if ($classname -eq $CSSEvenClass) {
            $classname = $CssOddClass
        } else {
            $classname = $CSSEvenClass
        }
        $class = $xml.CreateAttribute('class')
        $class.value = $classname
        $tr.attributes.append($class) | Out-null
    }
    $xml.innerxml | out-string
}
Function Convert-Size {

    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
        [Alias("Length")]
        [int64]$Size
    )
    Begin {
        If (-Not $ConvertSize) {
            Write-Verbose ("Creating signature from Win32API")
            $Signature =  @"
                 [DllImport("Shlwapi.dll", CharSet = CharSet.Auto)]
                 public static extern long StrFormatByteSize( long fileSize, System.Text.StringBuilder buffer, int bufferSize );
"@
            $Global:ConvertSize = Add-Type -Name SizeConverter -MemberDefinition $Signature -PassThru
        }
        Write-Verbose ("Building buffer for string")
        $stringBuilder = New-Object Text.StringBuilder 1024
    }
    Process {
        Write-Verbose ("Converting {0} to upper most size" -f $Size)
        $ConvertSize::StrFormatByteSize( $Size, $stringBuilder, $stringBuilder.Capacity ) | Out-Null
        $stringBuilder.ToString()
    }
}
#endregion Helper Functions

#region Load WSUS Required Assembly
If (-Not (Get-Module -ListAvailable -Name UpdateServices)) {
    #Add-Type "$Env:ProgramFiles\Update Services\Api\Microsoft.UpdateServices.Administration.dll"
    $Null = [reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration")
} Else {
    Import-Module -Name UpdateServices
}
#endregion Load WSUS Required Assembly

#region CSS Layout
$head=@"
    <style> 
        h1 {
            text-align:center;
            border-bottom:1px solid #666666;
            color:#009933;
        }
		TABLE {
			TABLE-LAYOUT: auto; 
			FONT-SIZE: 100%; 
			WIDTH: 100%
		}
		* {
			margin:0
		}

		.pageholder {
			margin: 0px auto;
		}
					
		td {
			VERTICAL-ALIGN: TOP; 
			FONT-FAMILY: Tahoma
		}
					
		th {
			VERTICAL-ALIGN: TOP; 
			COLOR: #018AC0; 
			TEXT-ALIGN: left;
            background-color:DarkGrey;
            color:Black;
		}
        body {
            text-align:left;
            font-smoothing:always;
            width:100%;
        }
        .odd { background-color:#ffffff; }
        .even { background-color:#dddddd; }               
    </style>
"@
#endregion CSS Layout

#region Initial WSUS Connection
$ErrorActionPreference = 'Stop'
Try {
    $Wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($WSUSServer,$UseSSL,$Port)
} Catch {
    Write-warning "$($WSUSServer)<$($Port)>: $($_)"
    Break
}
$ErrorActionPreference = 'Continue'
#endregion Initial WSUS Connection

#region Pre-Stage -- Used in more than one location
$htmlFragment = ''
$WSUSConfig = $Wsus.GetConfiguration()
$WSUSStats = $Wsus.GetStatus()
$TargetGroups = $Wsus.GetComputerTargetGroups()
$EmptyTargetGroups = $TargetGroups | Where {
    $_.GetComputerTargets().Count -eq 0 -AND $_.Name -ne 'Unassigned Computers'
}

#Stale Computers
$computerscope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
$computerscope.ToLastReportedStatusTime = (Get-Date).AddDays(-$DaysComputerStale)
$StaleComputers = $wsus.GetComputerTargets($computerscope) | ForEach {
    [pscustomobject]@{
        "Nombre del Equipo" = $_.FullDomainName
        IP = $_.IPAddress
        "Ultimo Contacto" = $_.LastReportedStatusTime
        "Ultima Sincronizacion" = $_.LastSyncTime
        Grupo = ($_.GetComputerTargetGroups() | Where-Object {$_.Name -ne "Todos los Equipos"} | Select -Expand Name) -join ', '
    }
}

#Pending Reboots
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope.IncludedInstallationStates = 'InstalledPendingReboot'
$computerScope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
$computerScope.IncludedInstallationStates = 'InstalledPendingReboot'
$GroupRebootHash=@{}
$ComputerPendingReboot = $wsus.GetComputerTargets($computerScope) | ForEach {
    $Update = ($_.GetUpdateInstallationInfoPerUpdate($updateScope) | ForEach {
        $Update = $_.GetUpdate()
        $Update.title
    }) -join ', '
    If ($Update) {
        $TempTargetGroups = ($_.GetComputerTargetGroups() | Where-Object {$_.Name -ne "Todos los Equipos"} | Select -Expand Name) -join ', '
        $TempTargetGroups | ForEach {
            $GroupRebootHash[$_]++
        }
        [pscustomobject] @{
            "Nombre del equipo" = $_.FullDomainName
            IP = $_.IPAddress
            Grupo = $TempTargetGroups -join ', '
            #Updates = $Update
        }
    }
} | Sort Computername


#NotInstalled
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope.IncludedInstallationStates = 'NotInstalled'
$computerScope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
$computerScope.IncludedInstallationStates = 'NotInstalled'
$GroupNotInstalledHash=@{}
$ComputerHash = @{}
$UpdateHash = @{}
$ComputerNotInstall = $wsus.GetComputerTargets($computerScope) | ForEach {
    $Computername = $_.FullDomainName
    $Update = ($_.GetUpdateInstallationInfoPerUpdate($updateScope) | ForEach {
        $Update = $_.GetUpdate()
        $Update.title
        $ComputerHash[$Computername] += ,$Update.title
        $UpdateHash[$Update.title] += ,$Computername
    }) -join ', '
    If ($Update) {
        $TempTargetGroups = ($_.GetComputerTargetGroups() | Where-Object {$_.Name -ne "Todos los Equipos"} | Select -Expand Name) -join ', '
        $TempTargetGroups | ForEach {
            $GroupNotInstalledHash[$_]++
        }
        [pscustomobject] @{
            "Nombre del Equipo" = $_.FullDomainName
            IP = $_.IPAddress
            Grupo = $TempTargetGroups -join ', '
            Actualizaciones = $Update
        }
    }
} | Sort Computername



#Failed Installations
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope.IncludedInstallationStates = 'Failed'
$computerScope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
$computerScope.IncludedInstallationStates = 'Failed'
$GroupFailHash=@{}
$ComputerHash = @{}
$UpdateHash = @{}
$ComputerFailInstall = $wsus.GetComputerTargets($computerScope) | ForEach {
    $Computername = $_.FullDomainName
    $Update = ($_.GetUpdateInstallationInfoPerUpdate($updateScope) | ForEach {
        $Update = $_.GetUpdate()
        $Update.title
        $ComputerHash[$Computername] += ,$Update.title
        $UpdateHash[$Update.title] += ,$Computername
    }) -join ', '
    If ($Update) {
        $TempTargetGroups = ($_.GetComputerTargetGroups() | Where-Object {$_.Name -ne "Todos los Equipos"} | Select -Expand Name) -join ', '
        $TempTargetGroups | ForEach {
            $GroupFailHash[$_]++
        }
        [pscustomobject] @{
            "Nombre del Equipo" = $_.FullDomainName
            IP = $_.IPAddress
            Grupo = $TempTargetGroups -join ', '
            Actualizaciones = $Update
        }
    }
} | Sort Computername
#endregion Pre-Stage -- Used in more than one location

#region WSUS SERVER INFORMATION
$Pre = @"
<div style='margin: 0px auto; BACKGROUND-COLOR:Blue;Color:White;font-weight:bold;FONT-SIZE: 16pt;'>
    Informacion del servidor WSUS
</div>
"@
    #region WSUS Version
                    $WSUSVersion = [pscustomobject]@{
    Servidor = $WSUS.ServerName
    Version = $Wsus.Version
    Puerto = $Wsus.PortNumber
    "Protocolo de Version del Servidor" = $Wsus.ServerProtocolVersion
    }
    $Pre += @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Informacion del Servicio WSUS
        </div>

"@
    $Body = $WSUSVersion | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post
    #endregion WSUS Version

    #region WSUS Server Content
    $drive = $WSUSConfig.LocalContentCachePath.Substring(0,2)
    $Data = Get-CIMInstance -ComputerName $WSUSServer -ClassName Win32_LogicalDisk -Filter "DeviceID='$drive'"
    $UsedSpace = $data.Size - $data.Freespace
    $PercentFree = "{0:P}" -f ($Data.Freespace / $Data.Size)
    $Pre = @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Informacion del almacenamiento de WSUS
        </div>

"@
    $WSUSDrive = [pscustomobject]@{
        "Ruta de archivos" = $WSUSConfig.LocalContentCachePath
        "Espacio total" = $data.Size | Convert-Size
        "Espacio Utilizado" = $UsedSpace | Convert-Size
        "Espacio Libre" = $Data.freespace | Convert-Size
        "% libre" = $PercentFree
    }
    $Body = $WSUSDrive | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post
    #endregion WSUS Server Content

    #region Last Synchronization
    $synch = $wsus.GetSubscription()
    $SynchHistory = $Synch.GetSynchronizationHistory()[0]
    $WSUSSynch = [pscustomobject]@{
        Automatica = $synch.SynchronizeAutomatically
        "Hora Programada" = $synch.SynchronizeAutomaticallyTimeOfDay
        "Ultima Sincronizacion" = $synch.LastSynchronizationTime
        Resultado = $SynchHistory.Result
    }
    If ($SynchHistory.Result -eq 'Failed') {
        $WSUSSynch = $WSUSSynch | Add-Member -MemberType NoteProperty -Name ErrorType -Value $SynchHistory.Error -PassThru |
        Add-Member -MemberType NoteProperty -Name ErrorText -Value $SynchHistory.ErrorText -PassThru
    }
    $Pre = @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Ultima sincronizacion del servidor
        </div>

"@
    $Body = $WSUSSynch | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post
    #endregion Last Synchronization

#endregion WSUS SERVER INFORMATION

#region CLIENT INFORMATION
$Pre = @"
<div style='margin: 0px auto; BACKGROUND-COLOR:Blue;Color:White;font-weight:bold;FONT-SIZE: 16pt;'>
    Informacion de Equipos
</div>
"@
    #region Computer Statistics
    $WSUSComputerStats = [pscustomobject]@{
        "Total equipos" = [int]$WSUSStats.ComputerTargetCount    
        "Stale($DaysComputerStale Days)" = ($StaleComputers | Measure-Object).count
        "Equipos pendientes de actualizar" = [int]$WSUSStats.ComputerTargetsNeedingUpdatesCount
        "Instalaciones con errores" = [int]$WSUSStats.ComputerTargetsWithUpdateErrorsCount
        "Pendientes de reiniciar" = ($ComputerPendingReboot | Measure-Object).Count
    }

    $Pre += @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Estadisticas de Equipos
        </div>

"@
    $Body = $WSUSComputerStats | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post
    #endregion Computer Statistics

    #region Operating System
    $Pre = @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Contador de equipos por Sistema Operativo
        </div>

"@
    $Body = $wsus.GetComputerTargets() | Group OSDescription |
    Select @{L='Sistema Operativo';E={$_.Name}}, Count  | 
    ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'Odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post    
    #endregion Operating System

    #region Stale Computers
    $Pre = @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Equipos sin comunicacion ($DaysComputerStale dias)
        </div>

"@
    $Body = $StaleComputers | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post
    #endregion Stale Computers

    #region Unassigned Computers
    $Unassigned = ($TargetGroups | Where {
        $_.Name -eq 'Equipos sin Asignar'
    }).GetComputerTargets() | ForEach {
        [pscustomobject]@{
            Equipo = $_.FullDomainName
            "Sistema Operativo" = $_.OSDescription
            IP = $_.IPAddress
            "Ultimo Contacto" = $_.LastReportedStatusTime
            "Ultima Sincronizacion" = $_.LastSyncTime
        }    
    }
    $Pre = @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Equipos sin Asignar
        </div>

"@
    $Body = $Unassigned | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post
    #endregion Unassigned Computers

    #region Failed Update Install
    $Pre = @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Equipos con errores de instalacion
        </div>

"@
    $Body = $ComputerFailInstall | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post
    #endregion Failed Update Install

    #region Pending Reboot 
    $Pre = @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Equipos pendientes de reiniciar
        </div>

"@
    $Body = $ComputerPendingReboot | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post
    #endregion Pending Reboot

    #region Not Installed 
    $Pre = @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Equipos con actualizaciones pendientes
        </div>

"@
    $Body = $ComputerNotInstall | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post
    #endregion Not Installed






#endregion CLIENT INFORMATION

#region UPDATE INFORMATION
$Pre = @"
<div style='margin: 0px auto; BACKGROUND-COLOR:Blue;Color:White;font-weight:bold;FONT-SIZE: 16pt;'>
    Informacion de Actualizaciones
</div>
"@
    #region Update Statistics
    $WSUSUpdateStats = [pscustomobject]@{
        Totales = [int]$WSUSStats.UpdateCount    
        Necesarias = [int]$WSUSStats.UpdatesNeededByComputersCount
        Aprobadas = [int]$WSUSStats.ApprovedUpdateCount
        Rechazadas = [int]$WSUSStats.DeclinedUpdateCount
        "Necesitan Ficheros" = [int]$WSUSStats.ExpiredUpdateCount 
        "Pendientes de Aprobar" = [int]$WSUSStats.NotApprovedUpdateCount   
    }
    $Pre += @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Estadisticas de Actualizaciones
        </div>

"@
    $Body = $WSUSUpdateStats | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post
    #endregion Update Statistics

    #region Failed Update Installations
    $FailedUpdateInstall = $UpdateHash.GetEnumerator() | ForEach {
        [pscustomobject]@{
            Actualizacion = $_.Name
            "Nombre del Equipo" = ($_.Value) -join ', '
        }
    }
    $Pre = @"
        <div style='margin: 0px auto; BACKGROUND-COLOR:LightBlue;Color:Black;font-weight:bold;FONT-SIZE: 14pt;'>
            Instalaciones fallidas por actualizacion
        </div>

"@
    $Body = $FailedUpdateInstall | ConvertTo-Html -Fragment | Out-String | Set-AlternatingCSSClass -CSSEvenClass 'even' -CssOddClass 'odd'
    $Post = "<br>"
    $htmlFragment += $Pre,$Body,$Post
    #endregion Failed Update Installations

#endregion UPDATE INFORMATION


#region Compile HTML Report
$HTMLParams = @{
    Head = $Head
    Title = "WSUS Report for $WSUSServer"
    PreContent = "<H1><font color='white'>Please view in html!</font><br>$WSUSServer WSUS Report</H1>"
    PostContent = "$($htmlFragment)<i>Report generated on $((Get-Date).ToString())</i>" 
    
}
$Report = ConvertTo-Html @HTMLParams | Out-File -FilePath c:\WSUS.html
#endregion Compile HTML Report

If ($ShowFile) {
    $Report | Out-File WSUSReport.html
    Invoke-Item WSUSReport.html
}

#region Send Email
If ($SendEmail) {
    $EmailParams.Body = $Report
    Send-MailMessage @EmailParams
}
#endregion Send Email