<# ReporteParche.ps1
Obtiene el último parche acumulativo visible por Win32_QuickFixEngineering en múltiples VMs,
añade edición de Windows, IP(s) del equipo, y calcula el último KB de seguridad y su fecha.
Resiliente a errores y con registro a archivo.
#>
[CmdletBinding()]
param(
[string]$RutaComputadores = ".\computers.txt",
[string]$SalidaCsv = "D:\Temp\Reporte_ParcheAcumulativo.csv",
[pscredential]$Credencial = $null
)
$ErrorActionPreference = 'Stop'
# Asegura carpeta de salida
$salidaDir = Split-Path -Parent $SalidaCsv
if (-not (Test-Path $salidaDir)) { New-Item -Path $salidaDir -ItemType Directory -Force | Out-Null }
# Comienza transcripción de la sesión
$logPath = Join-Path -Path $salidaDir -ChildPath ("ReporteParche_{0:yyyyMMdd_HHmmss}.log" -f (Get-Date))
try { Start-Transcript -Path $logPath -Force | Out-Null } catch {}
# Carga equipos
$Computers = @(
"q2crtaspa01","q2crtassop01","q2crtcsera01","q2crtcserb02","q2crtfiIp02","q2crthist01","q2crtiecser2",
"q2crtinfp70","q2crtinfp95","q2crtinfp96","q2crtjmp01","q2crtjmp02","q2crtopcda01","q2crtopcda02",
"q2crtopcda03","q2crtopcda04","q2crtopcda05","q2crtopcua01","q2crtpicnn01","q2crtpicnn02","q2crtpicnn03",
"q2crtpicnn04","q2crtpiint01","q2crtpiint02","q2crtpimgr01","q2crtpirIy01","q2crtplcpcs1","q2crtsqIp01",
"q2crtteap01","q2crtuagp01","q2crtuagp02","q2crtvrop01"
)
if (Test-Path $RutaComputadores) {
$Computers = Get-Content -Path $RutaComputadores | Where-Object { $_ -and $_.Trim() } | ForEach-Object { $_.Trim() }
}
if (-not $Computers -or $Computers.Count -eq 0) {
Write-Error "No se definieron equipos. Proporciona un archivo de equipos o rellena el arreglo en el script."
try { Stop-Transcript | Out-Null } catch {}
exit 1
}
# Script remoto
$scriptBlock = {
param($useCred)
$ErrorActionPreference = 'Stop'
try {
# SO
$cv = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$displayVersion = $cv.DisplayVersion; if (-not $displayVersion) { $displayVersion = $cv.ReleaseId }
$build = "{0}.{1}" -f $cv.CurrentBuild, $cv.UBR
$ubr = $cv.UBR
$prod = $cv.ProductName
$edId = $cv.EditionID
$osEd = if ($prod -and $edId) { "{0} ({1})" -f $prod, $edId } elseif ($prod) { $prod } else { $edId }
# IPv4
$ipList = @()
try {
$ipList = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled = TRUE" |
ForEach-Object { $_.IPAddress } |
Where-Object { $_ -match '^\d+\.' -and $_ -notmatch '^(169\.254|127\.)' }
} catch {}
$ipStr = ($ipList | Select-Object -Unique) -join ';'
# QFE y Seguridad
$qfe = Get-CimInstance -ClassName Win32_QuickFixEngineering -ErrorAction Stop |
Sort-Object @{e={ $_.InstalledOn -as [datetime] }}, InstalledOn -Descending
$ultimo = $null; $kb = $null
$secUlt = $null; $kbSec = $null
if ($qfe) {
$ultimo = $qfe | Select-Object -First 1
$kb = $ultimo.HotFixID
if ($kb -notmatch 'KB\d+') {
$m = [regex]::Match("$kb $($ultimo.Description) $($ultimo.Caption)", 'KB\d+')
if ($m.Success) { $kb = $m.Value }
}
$qfeSec = $qfe | Where-Object {
($_.Description -match '(?i)security|seguridad') -or
($_.Caption -match '(?i)security|seguridad')
}
if ($qfeSec) {
$secUlt = $qfeSec | Select-Object -First 1
$kbSec = $secUlt.HotFixID
if ($kbSec -notmatch 'KB\d+') {
$m2 = [regex]::Match("$kbSec $($secUlt.Description) $($secUlt.Caption)", 'KB\d+')
if ($m2.Success) { $kbSec = $m2.Value }
}
}
}
if (-not $qfe) {
return [pscustomobject]@{
ComputerName = $env:COMPUTERNAME
IP = $ipStr
Estado = "Sin datos de QFE"
UltimoKB = $null
FechaInstalacion = $null
UltimoKBSeguridad = $null
FechaKBSeguridad = $null
OSVersion = $displayVersion
OSEdition = $osEd
OSBuild = $build
UBR = $ubr
Origen = "QFE"
}
}
[pscustomobject]@{
ComputerName = $env:COMPUTERNAME
IP = $ipStr
Estado = "OK"
UltimoKB = $kb
FechaInstalacion = ($ultimo.InstalledOn -as [datetime])
UltimoKBSeguridad = $kbSec
FechaKBSeguridad = ($secUlt.InstalledOn -as [datetime])
OSVersion = $displayVersion
OSEdition = $osEd
OSBuild = $build
UBR = $ubr
Origen = "QFE"
}
}
catch {
# Intento capturar IP aún con error
$ipStr = $null
try {
$ipStr = (Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled = TRUE" |
ForEach-Object { $_.IPAddress } |
Where-Object { $_ -match '^\d+\.' -and $_ -notmatch '^(169\.254|127\.)' } |
Select-Object -Unique) -join ';'
} catch {}
[pscustomobject]@{
ComputerName = $env:COMPUTERNAME
IP = $ipStr
Estado = "Error: $($_.Exception.Message)"
UltimoKB = $null
FechaInstalacion = $null
UltimoKBSeguridad = $null
FechaKBSeguridad = $null
OSVersion = $null
OSEdition = $null
OSBuild = $null
UBR = $null
Origen = "QFE"
}
}
}
# Consulta por equipo
$Resultados = New-Object System.Collections.Generic.List[object]
foreach ($c in $Computers) {
Write-Verbose ("Procesando {0}…" -f $c)
try {
$wsOk = $false
try {
Test-WSMan -ComputerName $c -ErrorAction Stop | Out-Null
$wsOk = $true
} catch {
Write-Verbose ("WSMan no disponible en {0}: {1}" -f $c, $_.Exception.Message)
}
if ($wsOk) {
$icParams = @{ ComputerName = $c; ScriptBlock = $scriptBlock; ArgumentList = $true; ErrorAction = 'Stop' }
if ($Credencial) { $icParams.Credential = $Credencial }
$res = Invoke-Command @icParams
$Resultados.Add($res) | Out-Null
continue
}
# DCOM
$cimOpts = New-CimSessionOption -Protocol Dcom
$cim = if ($Credencial) {
New-CimSession -ComputerName $c -SessionOption $cimOpts -Credential $Credencial -ErrorAction Stop
} else {
New-CimSession -ComputerName $c -SessionOption $cimOpts -ErrorAction Stop
}
try {
# IP
$ipStr = $null
try {
$ipStr = (Get-CimInstance -CimSession $cim Win32_NetworkAdapterConfiguration -Filter "IPEnabled = TRUE" |
ForEach-Object { $_.IPAddress } |
Where-Object { $_ -match '^\d+\.' -and $_ -notmatch '^(169\.254|127\.)' } |
Select-Object -Unique) -join ';'
} catch {}
# QFE
$qfe = Get-CimInstance -CimSession $cim -ClassName Win32_QuickFixEngineering -ErrorAction Stop |