Descripción General
Este script de PowerShell proporciona una interfaz gráfica avanzada para buscar software instalado en equipos del dominio. Permite consultar remotamente el registro, proveedores de paquetes y WMI para identificar aplicaciones instaladas, además de ofrecer opciones para instalación desatendida y desinstalación remota.
Video demostrativo
Puedes ver la demostración completa de la herramienta en Odysee directamente desde esta página o abriendo el video en una pestaña nueva.
Funciones Principales
Búsqueda remota de software
Get-InstalledSoftwareRemote
Orquesta la búsqueda de software instalado en un equipo remoto utilizando registro, Get-Package y Win32_Product como último recurso.
Get-RemoteSoftwareFromRegistry / Package / WMI
Funciones auxiliares que extraen la información de software desde el registro, proveedores de paquetes y WMI.
Instalación y desinstalación remota
Invoke-RemoteInstall
Permite copiar un instalador al equipo remoto y ejecutarlo de forma silenciosa, verificando la instalación mediante el registro.
Invoke-RemoteUninstall
Realiza desinstalaciones en cascada usando QuietUninstallString, UninstallString y Win32_Product según disponibilidad.
Instrucciones de Uso
- Cargar los equipos del dominio mediante el botón correspondiente.
- Escribir el nombre o parte del nombre del software a buscar.
- Seleccionar uno o varios equipos de la lista.
- Pulsar en "Buscar en seleccionados" para iniciar la consulta remota.
- Utilizar las opciones de exportación a CSV o copia de filas según sea necesario.
- Opcionalmente, configurar un instalador y usar los botones de instalación o desinstalación remota.
Ejemplo de Uso
Escenario típico:
- Buscar un software concreto (por ejemplo, "Chrome" o "Office") en un conjunto de servidores o estaciones de trabajo.
- Ver en qué equipos está instalado y qué versión tienen.
- Exportar los resultados a CSV para documentar el estado de la red.
- Desplegar una nueva versión del software en equipos seleccionados mediante la instalación remota.
Script Completo
A continuación se presenta el script completo de PowerShell para la búsqueda y administración remota de software. Puedes copiar el código completo usando el botón "Copiar" de tu navegador o editor.
#requires -Version 5.1
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
function Write-BuscarSoftwareLog {
param(
[Parameter(Mandatory)][string]$Message
)
try {
$logDirectory = 'C:\Logs'
$logPath = Join-Path -Path $logDirectory -ChildPath 'BuscarSoftwareRed.log'
if (-not (Test-Path -Path $logDirectory)) {
New-Item -Path $logDirectory -ItemType Directory -Force | Out-Null
}
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$entry = "[$timestamp] $Message"
Add-Content -Path $logPath -Value $entry -Encoding UTF8
}
catch {
# No romper la ejecución del módulo si falla el log
}
}
function Get-ImageFromBase64 {
param(
[Parameter(Mandatory)][string]$Base64
)
if ([string]::IsNullOrWhiteSpace($Base64)) {
return $null
}
try {
$bytes = [Convert]::FromBase64String($Base64)
$ms = New-Object System.IO.MemoryStream(, $bytes)
return [System.Drawing.Image]::FromStream($ms)
}
catch {
return $null
}
}
function Get-DomainComputers {
param(
[ValidateSet('All', 'Servers', 'Workstations')]
[string]$Type = 'All'
)
try {
Import-Module ActiveDirectory -ErrorAction Stop
}
catch {
[System.Windows.Forms.MessageBox]::Show("No se pudo importar el módulo ActiveDirectory.\nAsegúrate de tener RSAT/AD
instalado.", "Error", 'OK', 'Error') | Out-Null
return @()
}
try {
$ldapFilter = '(objectClass=computer)'
switch ($Type) {
'Servers' {
$ldapFilter = '(operatingSystem=*Server*)'
}
'Workstations' {
$ldapFilter = '(!(operatingSystem=*Server*))'
}
}
$computers = Get-ADComputer -LDAPFilter $ldapFilter -Properties OperatingSystem |
Select-Object Name, OperatingSystem
return $computers | Sort-Object Name
}
catch {
$errorMessage = $_.Exception.Message
$fullError = $_ | Out-String
Write-BuscarSoftwareLog -Message "Get-DomainComputers error. Type=$Type; Message=$errorMessage; Details=$fullError"
[System.Windows.Forms.MessageBox]::Show("Error al obtener equipos del dominio. Detalle: $errorMessage", "Error", 'OK',
'Error') | Out-Null
return @()
}
}
function Get-RemoteSoftwareFromRegistry {
param(
[Parameter(Mandatory)][string]$ComputerName,
[Parameter(Mandatory)][string]$SearchText
)
$scriptBlock = {
param($SearchTextInner)
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$apps = foreach ($path in $paths) {
Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and ($_.DisplayName -like "*" + $SearchTextInner + "*") } |
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate
}
return $apps
}
try {
$sessionOptions = New-PSSessionOption -OperationTimeout 120000
$result = Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $SearchText -SessionOption
$sessionOptions -ErrorAction Stop
foreach ($app in $result) {
[PSCustomObject]@{
ComputerName = $ComputerName
DisplayName = $app.DisplayName
DisplayVersion = $app.DisplayVersion
Publisher = $app.Publisher
InstallDate = $app.InstallDate
}
}
}
catch {
$errorMessage = $_.Exception.Message
$fullError = $_ | Out-String
Write-BuscarSoftwareLog -Message "Get-RemoteSoftwareFromRegistry error. Computer=$ComputerName; Message=$errorMessage;
Details=$fullError"
@()
}
}
function Get-RemoteSoftwareFromPackage {
param(
[Parameter(Mandatory)][string]$ComputerName,
[Parameter(Mandatory)][string]$SearchText
)
$scriptBlock = {
param($SearchTextInner)
try {
$packages = Get-Package -ErrorAction Stop |
Where-Object { $_.Name -and ($_.Name -like "*" + $SearchTextInner + "*") }
foreach ($pkg in $packages) {
[PSCustomObject]@{
DisplayName = $pkg.Name
DisplayVersion = $pkg.Version
Publisher = $pkg.ProviderName
InstallDate = $null
}
}
}
catch {
@()
}
}
try {
$result = Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $SearchText -ErrorAction
Stop
foreach ($app in $result) {
[PSCustomObject]@{
ComputerName = $ComputerName
DisplayName = $app.DisplayName
DisplayVersion = $app.DisplayVersion
Publisher = $app.Publisher
InstallDate = $app.InstallDate
}
}
}
catch {
$errorMessage = $_.Exception.Message
$fullError = $_ | Out-String
Write-BuscarSoftwareLog -Message "Get-RemoteSoftwareFromPackage error. Computer=$ComputerName; Message=$errorMessage;
Details=$fullError"
@()
}
}
function Get-RemoteSoftwareFromWmi {
param(
[Parameter(Mandatory)][string]$ComputerName,
[Parameter(Mandatory)][string]$SearchText
)
try {
$instances = Get-CimInstance -ClassName Win32_Product -ComputerName $ComputerName -OperationTimeoutSec 120 -ErrorAction
Stop |
Where-Object { $_.Name -and ($_.Name -like "*" + $SearchText + "*") }
foreach ($inst in $instances) {
[PSCustomObject]@{
ComputerName = $ComputerName
DisplayName = $inst.Name
DisplayVersion = $inst.Version
Publisher = $inst.Vendor
InstallDate = $inst.InstallDate
}
}
}
catch {
$errorMessage = $_.Exception.Message
$fullError = $_ | Out-String
Write-BuscarSoftwareLog -Message "Get-RemoteSoftwareFromWmi error. Computer=$ComputerName; Message=$errorMessage;
Details=$fullError"
@()
}
}
function Get-InstalledSoftwareRemote {
param(
[Parameter(Mandatory)] [string]$ComputerName,
[Parameter(Mandatory)] [string]$SearchText,
[switch]$UseWin32Product = $true
)
# 0) Comprobar que el equipo responde a ICMP
if (-not (Test-ComputerOnline -ComputerName $ComputerName)) {
Write-BuscarSoftwareLog -Message "Equipo no accesible por ICMP (posible problema de DNS, apagado o firewall ICMP):
$ComputerName"
return @()
}
$results = @()
# 1) Registro (método principal)
$results += Get-RemoteSoftwareFromRegistry -ComputerName $ComputerName -SearchText $SearchText
# 2) Get-Package como fallback si no hubo resultados
if (-not $results -or $results.Count -eq 0) {
$results += Get-RemoteSoftwareFromPackage -ComputerName $ComputerName -SearchText $SearchText
}
# 3) WMI (Win32_Product) como último recurso automático si no hay resultados
if ($UseWin32Product -and (-not $results -or $results.Count -eq 0)) {
$results += Get-RemoteSoftwareFromWmi -ComputerName $ComputerName -SearchText $SearchText
}
if (-not $results -or $results.Count -eq 0) {
return @()
}
return $results
}
function Get-DefaultSilentArguments {
param(
[Parameter(Mandatory)][string]$InstallerPath
)
$ext = [System.IO.Path]::GetExtension($InstallerPath).ToLowerInvariant()
switch ($ext) {
'.msi' { '/i `"{0}`" /qn /norestart' -f $InstallerPath }
'.exe' { '/silent /norestart' }
default { '' }
}
}
function Invoke-RemoteInstall {
param(
[Parameter(Mandatory)][string]$ComputerName,
[Parameter(Mandatory)][string]$InstallerPath,
[string]$SilentArgs,
[string]$CustomCommand,
[string]$ExpectedDisplayName
)
if (-not (Test-ComputerOnline -ComputerName $ComputerName)) {
Write-BuscarSoftwareLog -Message "Instalacion omitida. Equipo sin respuesta ICMP: $ComputerName"
return $false
}
if (-not (Test-Path -Path $InstallerPath)) {
Write-BuscarSoftwareLog -Message "Ruta de instalador no valida: $InstallerPath"
return $false
}
$fileName = [System.IO.Path]::GetFileName($InstallerPath)
$ext = [System.IO.Path]::GetExtension($InstallerPath).ToLowerInvariant()
if ([string]::IsNullOrWhiteSpace($SilentArgs)) {
$SilentArgs = Get-DefaultSilentArguments -InstallerPath $InstallerPath
}
$session = $null
try {
$session = New-PSSession -ComputerName $ComputerName -ErrorAction Stop
$remoteFolder = 'C:\Temp\SoftwareDeploy'
Invoke-Command -Session $session -ScriptBlock {
param($folder)
if (-not (Test-Path -Path $folder)) {
New-Item -Path $folder -ItemType Directory -Force | Out-Null
}
} -ArgumentList $remoteFolder -ErrorAction Stop
$uniqueName = ([guid]::NewGuid().ToString('N').Substring(0, 8) + '_' + $fileName)
$remotePath = Join-Path -Path $remoteFolder -ChildPath $uniqueName
Copy-Item -Path $InstallerPath -Destination $remotePath -ToSession $session -Force
$commandLine = $null
if (-not [string]::IsNullOrWhiteSpace($CustomCommand)) {
$commandLine = $CustomCommand.Replace('{InstallerPath}', '"{0}"' -f $remotePath)
}
else {
if ($ext -eq '.msi') {
if (-not $SilentArgs) { $SilentArgs = '/qn /norestart' }
$commandLine = "msiexec.exe /i `"$remotePath`" $SilentArgs"
}
else {
$commandLine = '"' + $remotePath + '" ' + $SilentArgs
}
}
$scriptBlock = {
param($cmd, $expectedName)
Start-Process -FilePath 'cmd.exe' -ArgumentList "/c $cmd" -WindowStyle Hidden -Wait -ErrorAction Stop
$installedOk = $true
if ($expectedName -and -not [string]::IsNullOrWhiteSpace($expectedName)) {
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$found = $false
foreach ($p in $paths) {
$item = Get-ItemProperty -Path $p -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and ($_.DisplayName -like "*" + $expectedName + "*") } |
Select-Object -First 1 DisplayName
if ($item) {
$found = $true
break
}
}
if (-not $found) {
$installedOk = $false
}
}
return $installedOk
}
$remoteResult = Invoke-Command -Session $session -ScriptBlock $scriptBlock -ArgumentList $commandLine,
$ExpectedDisplayName -ErrorAction Stop
if ($remoteResult) {
Write-BuscarSoftwareLog -Message "Instalacion verificada via PSSession en $ComputerName. Comando: $commandLine"
return $true
}
else {
Write-BuscarSoftwareLog -Message "Instalador se ejecuto pero no se encontro el software esperado en el registro en
$ComputerName. ExpectedDisplayName: $ExpectedDisplayName"
return $false
}
}
catch {
$msg = $_.Exception.Message
Write-BuscarSoftwareLog -Message "Error en instalacion remota para $ComputerName. Detalle: $msg"
return $false
}
finally {
if ($session) {
try { Remove-PSSession -Session $session -ErrorAction SilentlyContinue } catch { }
}
}
}
function Invoke-RemoteUninstall {
param(
[Parameter(Mandatory)][string]$ComputerName,
[Parameter(Mandatory)][string]$DisplayName
)
if (-not (Test-ComputerOnline -ComputerName $ComputerName)) {
Write-BuscarSoftwareLog -Message "Desinstalacion omitida. Equipo sin respuesta ICMP: $ComputerName"
return $false
}
$methods = @('RegistryQuiet', 'RegistryNormal', 'Win32Product')
foreach ($method in $methods) {
try {
switch ($method) {
'RegistryQuiet' {
$scriptBlock = {
param($name)
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
foreach ($path in $paths) {
Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and ($_.DisplayName -like "*" + $name + "*") -and $_.QuietUninstallString } |
Select-Object -First 1 @{ Name = 'Command'; Expression = { $_.QuietUninstallString } }
}
}
$cmdObj = Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $DisplayName -ErrorAction
Stop
if ($cmdObj -and $cmdObj.Command) {
$cmd = [string]$cmdObj.Command
$sb = {
param($uCmd)
Start-Process -FilePath 'cmd.exe' -ArgumentList "/c $uCmd" -WindowStyle Hidden -Wait -ErrorAction Stop
}
Invoke-Command -ComputerName $ComputerName -ScriptBlock $sb -ArgumentList $cmd -ErrorAction Stop
Write-BuscarSoftwareLog -Message "Desinstalacion exitosa (QuietUninstallString) en $ComputerName. DisplayName:
$DisplayName Comando: $cmd"
return $true
}
}
'RegistryNormal' {
$scriptBlock2 = {
param($name)
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
foreach ($path in $paths) {
Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and ($_.DisplayName -like "*" + $name + "*") -and $_.UninstallString } |
Select-Object -First 1 DisplayName, UninstallString
}
}
$info = Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock2 -ArgumentList $DisplayName -ErrorAction
Stop
if ($info -and $info.UninstallString) {
$u = [string]$info.UninstallString
if ($u -match 'msiexec(.+?)/I\s*\{') {
$u = $u -replace '/I', '/x'
if ($u -notmatch '/qn') {
$u += ' /qn /norestart'
}
}
$sb2 = {
param($uCmd)
Start-Process -FilePath 'cmd.exe' -ArgumentList "/c $uCmd" -WindowStyle Hidden -Wait -ErrorAction Stop
}
Invoke-Command -ComputerName $ComputerName -ScriptBlock $sb2 -ArgumentList $u -ErrorAction Stop
Write-BuscarSoftwareLog -Message "Desinstalacion iniciada (UninstallString) en $ComputerName. DisplayName: $DisplayName
Comando: $u"
return $true
}
}
'Win32Product' {
$sb3 = {
param($name)
$product = Get-CimInstance -ClassName Win32_Product -ErrorAction Stop |
Where-Object { $_.Name -and ($_.Name -like "*" + $name + "*") } |
Select-Object -First 1
if ($product) {
$result = $product.Uninstall()
return $result.ReturnValue
}
else {
return $null
}
}
$rv = Invoke-Command -ComputerName $ComputerName -ScriptBlock $sb3 -ArgumentList $DisplayName -ErrorAction Stop
if ($rv -eq 0) {
Write-BuscarSoftwareLog -Message "Desinstalacion exitosa via Win32_Product en $ComputerName. DisplayName: $DisplayName"
return $true
}
}
}
}
catch {
$msg = $_.Exception.Message
Write-BuscarSoftwareLog -Message "Metodo de desinstalacion '$method' fallo para $ComputerName. DisplayName: $DisplayName
Detalle: $msg"
}
}
Write-BuscarSoftwareLog -Message "Todos los metodos de desinstalacion fallaron para $ComputerName. DisplayName:
$DisplayName"
return $false
}
# Crear formulario
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Buscador de software en la red'
$form.Size = New-Object System.Drawing.Size(900, 780)
$form.StartPosition = 'CenterScreen'
$form.TopMost = $false
$(
# Estilo general del formulario
$form.BackColor = [System.Drawing.Color]::FromArgb(245, 247, 250)
$form.Font = New-Object System.Drawing.Font('Segoe UI', 9)
$form.FormBorderStyle = 'FixedDialog'
$form.MaximizeBox = $false
)
# Etiqueta y textbox de búsqueda
$lblSearch = New-Object System.Windows.Forms.Label
$lblSearch.Text = 'Nombre (o parte) del software:'
$lblSearch.AutoSize = $true
$lblSearch.Location = New-Object System.Drawing.Point(10, 90)
$lblSearch.ForeColor = [System.Drawing.Color]::FromArgb(45, 52, 63)
$txtSearch = New-Object System.Windows.Forms.TextBox
$txtSearch.Location = New-Object System.Drawing.Point(190, 90)
$txtSearch.Width = 300
$txtSearch.BorderStyle = 'FixedSingle'
# Filtro de tipo de equipo
$lblType = New-Object System.Windows.Forms.Label
$lblType.Text = 'Tipo de equipo:'
$lblType.AutoSize = $true
$lblType.Location = New-Object System.Drawing.Point(10, 60)
$lblType.ForeColor = [System.Drawing.Color]::FromArgb(45, 52, 63)
$cmbType = New-Object System.Windows.Forms.ComboBox
$cmbType.Location = New-Object System.Drawing.Point(100, 55)
$cmbType.Width = 150
$cmbType.DropDownStyle = 'DropDownList'
$cmbType.FlatStyle = 'Flat'
[void]$cmbType.Items.Add('Todos')
[void]$cmbType.Items.Add('Servidores')
[void]$cmbType.Items.Add('Workstations')
$cmbType.SelectedIndex = 0
# Checkbox para uso de Win32_Product
$chkUseWin32Product = New-Object System.Windows.Forms.CheckBox
$chkUseWin32Product.Text = 'Usar Win32_Product (lento)'
$chkUseWin32Product.AutoSize = $true
$chkUseWin32Product.Location = New-Object System.Drawing.Point(500, 90)
$chkUseWin32Product.Checked = $true
$chkUseWin32Product.ForeColor = [System.Drawing.Color]::FromArgb(80, 80, 80)
# Controles para instalacion desatendida
$lblInstaller = New-Object System.Windows.Forms.Label
$lblInstaller.Text = 'Instalador:'
$lblInstaller.AutoSize = $true
$lblInstaller.Location = New-Object System.Drawing.Point(60, 600)
$lblInstaller.ForeColor = [System.Drawing.Color]::FromArgb(45, 52, 63)
$txtInstallerPath = New-Object System.Windows.Forms.TextBox
$txtInstallerPath.Location = New-Object System.Drawing.Point(125, 600)
$txtInstallerPath.Width = 360
$txtInstallerPath.BorderStyle = 'FixedSingle'
$btnBrowseInstaller = New-Object System.Windows.Forms.Button
$btnBrowseInstaller.Text = '...'
$btnBrowseInstaller.Location = New-Object System.Drawing.Point(488, 600)
$btnBrowseInstaller.Width = 40
$btnBrowseInstaller.FlatStyle = 'Flat'
$btnBrowseInstaller.BackColor = [System.Drawing.Color]::FromArgb(127, 140, 141)
$btnBrowseInstaller.ForeColor = [System.Drawing.Color]::White
$lblInstallArgs = New-Object System.Windows.Forms.Label
$lblInstallArgs.Text = 'Parametros silenciosos (opcional):'
$lblInstallArgs.AutoSize = $true
$lblInstallArgs.Location = New-Object System.Drawing.Point(60, 630)
$lblInstallArgs.ForeColor = [System.Drawing.Color]::FromArgb(45, 52, 63)
$txtInstallArgs = New-Object System.Windows.Forms.TextBox
$txtInstallArgs.Location = New-Object System.Drawing.Point(250, 630)
$txtInstallArgs.Width = 280
$txtInstallArgs.BorderStyle = 'FixedSingle'
$lblExpectedName = New-Object System.Windows.Forms.Label
$lblExpectedName.Text = 'Nombre esperado (registro):'
$lblExpectedName.AutoSize = $true
$lblExpectedName.Location = New-Object System.Drawing.Point(60, 660)
$lblExpectedName.ForeColor = [System.Drawing.Color]::FromArgb(45, 52, 63)
$txtExpectedName = New-Object System.Windows.Forms.TextBox
$txtExpectedName.Location = New-Object System.Drawing.Point(250, 658)
$txtExpectedName.Width = 280
$txtExpectedName.BorderStyle = 'FixedSingle'
# Botón cargar equipos
$btnLoadComputers = New-Object System.Windows.Forms.Button
$btnLoadComputers.Text = 'Cargar equipos'
$btnLoadComputers.Location = New-Object System.Drawing.Point(270, 55)
$btnLoadComputers.Width = 150
$btnLoadComputers.FlatStyle = 'Flat'
$btnLoadComputers.BackColor = [System.Drawing.Color]::FromArgb(52, 152, 219)
$btnLoadComputers.ForeColor = [System.Drawing.Color]::White
# Lista de equipos
$lblComputers = New-Object System.Windows.Forms.Label
$lblComputers.Text = 'Equipos del dominio:'
$lblComputers.AutoSize = $true
$lblComputers.Location = New-Object System.Drawing.Point(10, 130)
$lblComputers.ForeColor = [System.Drawing.Color]::FromArgb(45, 52, 63)
$lstComputers = New-Object System.Windows.Forms.ListBox
$lstComputers.Location = New-Object System.Drawing.Point(10, 160)
$lstComputers.Size = New-Object System.Drawing.Size(250, 400)
$lstComputers.SelectionMode = 'MultiExtended'
$lstComputers.BorderStyle = 'FixedSingle'
# Botón buscar
$btnSearch = New-Object System.Windows.Forms.Button
$btnSearch.Text = 'Buscar en seleccionados'
$btnSearch.Location = New-Object System.Drawing.Point(680, 90)
$btnSearch.Width = 180
$btnSearch.FlatStyle = 'Flat'
$btnSearch.BackColor = [System.Drawing.Color]::FromArgb(39, 174, 96)
$btnSearch.ForeColor = [System.Drawing.Color]::White
# Botón exportar a CSV
$btnExportCsv = New-Object System.Windows.Forms.Button
$btnExportCsv.Text = 'Exportar a CSV'
$btnExportCsv.Location = New-Object System.Drawing.Point(600, 570)
$btnExportCsv.Width = 130
$btnExportCsv.FlatStyle = 'Flat'
$btnExportCsv.BackColor = [System.Drawing.Color]::FromArgb(52, 73, 94)
$btnExportCsv.ForeColor = [System.Drawing.Color]::White
# Botón copiar fila
$btnCopySelected = New-Object System.Windows.Forms.Button
$btnCopySelected.Text = 'Copiar selección'
$btnCopySelected.Location = New-Object System.Drawing.Point(740, 570)
$btnCopySelected.Width = 130
$btnCopySelected.FlatStyle = 'Flat'
$btnCopySelected.BackColor = [System.Drawing.Color]::FromArgb(127, 140, 141)
$btnCopySelected.ForeColor = [System.Drawing.Color]::White
$btnInstall = New-Object System.Windows.Forms.Button
$btnInstall.Text = 'Instalar en seleccionados'
$btnInstall.Location = New-Object System.Drawing.Point(600, 600)
$btnInstall.Width = 190
$btnInstall.Height = 60
$btnInstall.FlatStyle = 'Flat'
$btnInstall.BackColor = [System.Drawing.Color]::FromArgb(231, 76, 60)
$btnInstall.ForeColor = [System.Drawing.Color]::White
$pictureLogo.Image = $logoImage
}
# DataGridView para resultados
$grid = New-Object System.Windows.Forms.DataGridView
$grid.Location = New-Object System.Drawing.Point(270, 160)
$grid.Size = New-Object System.Drawing.Size(600, 400)
$grid.ReadOnly = $true
$grid.AllowUserToAddRows = $false
$grid.AllowUserToDeleteRows = $false
$grid.AutoSizeColumnsMode = 'Fill'
$grid.BorderStyle = 'FixedSingle'
$grid.BackgroundColor = [System.Drawing.Color]::White
$grid.EnableHeadersVisualStyles = $false
$headerStyle = New-Object System.Windows.Forms.DataGridViewCellStyle
$headerStyle.BackColor = [System.Drawing.Color]::FromArgb(52, 73, 94)
$headerStyle.ForeColor = [System.Drawing.Color]::White
$headerStyle.Font = New-Object System.Drawing.Font('Segoe UI', 9, [System.Drawing.FontStyle]::Bold)
$grid.ColumnHeadersDefaultCellStyle = $headerStyle
$rowStyle = New-Object System.Windows.Forms.DataGridViewCellStyle
$rowStyle.BackColor = [System.Drawing.Color]::White
$rowStyle.SelectionBackColor = [System.Drawing.Color]::FromArgb(52, 152, 219)
$rowStyle.SelectionForeColor = [System.Drawing.Color]::White
$grid.DefaultCellStyle = $rowStyle
$altRowStyle = New-Object System.Windows.Forms.DataGridViewCellStyle
$altRowStyle.BackColor = [System.Drawing.Color]::FromArgb(245, 247, 250)
$grid.AlternatingRowsDefaultCellStyle = $altRowStyle
# Barra de progreso para carga/busqueda
$progressBar = New-Object System.Windows.Forms.ProgressBar
$progressBar.Location = New-Object System.Drawing.Point(60, 685)
$progressBar.Size = New-Object System.Drawing.Size(500, 15)
$progressBar.Style = 'Continuous'
$progressBar.Minimum = 0
$progressBar.Maximum = 100
$progressBar.Value = 0
$progressBar.Visible = $false
# Barra de progreso para instalacion/desinstalacion (parte inferior)
$progressBarInstall = New-Object System.Windows.Forms.ProgressBar
$progressBarInstall.Location = New-Object System.Drawing.Point(60, 685)
$progressBarInstall.Size = New-Object System.Drawing.Size(470, 15)
$progressBarInstall.Style = 'Continuous'
$progressBarInstall.Minimum = 0
$progressBarInstall.Maximum = 100
$progressBarInstall.Value = 0
$progressBarInstall.Visible = $false
# Label de estado
$lblStatus = New-Object System.Windows.Forms.Label
$lblStatus.Text = 'Listo.'
$lblStatus.AutoSize = $true
$lblStatus.Location = New-Object System.Drawing.Point(10, 560)
$lblStatus.ForeColor = [System.Drawing.Color]::FromArgb(90, 98, 110)
# Agregar controles al formulario
$form.Controls.AddRange(@(
$lblSearch,
$txtSearch,
$lblType,
$cmbType,
$btnLoadComputers,
$lblComputers,
$lstComputers,
$btnSearch,
$btnExportCsv,
$btnCopySelected,
$btnInstall,
$btnUninstall,
$pictureLogo,
$chkUseWin32Product,
$lblInstaller,
$txtInstallerPath,
$btnBrowseInstaller,
$lblInstallArgs,
$txtInstallArgs,
$lblExpectedName,
$txtExpectedName,
$grid,
$lblStatus,
$progressBar,
$progressBarInstall
))
# Evento: Cargar equipos
$btnLoadComputers.Add_Click({
$lblStatus.Text = 'Cargando equipos del dominio...'
$form.Refresh()
$btnLoadComputers.Enabled = $false
$btnSearch.Enabled = $false
$lstComputers.Enabled = $false
$progressBar.Style = 'Marquee'
$progressBar.MarqueeAnimationSpeed = 30
$progressBar.Visible = $true
switch ($cmbType.SelectedItem) {
'Servidores' { $type = 'Servers' }
'Workstations' { $type = 'Workstations' }
default { $type = 'All' }
}
$lstComputers.Items.Clear()
$computers = Get-DomainComputers -Type $type
if (-not $computers -or $computers.Count -eq 0) {
$lblStatus.Text = 'No se encontraron equipos o hubo un error.'
}
else {
foreach ($c in $computers) {
[void]$lstComputers.Items.Add($c.Name)
}
$lblStatus.Text = "Equipos cargados: $($computers.Count)"
}
$progressBar.Visible = $false
$progressBar.Style = 'Continuous'
$progressBar.Value = 0
$btnLoadComputers.Enabled = $true
$btnSearch.Enabled = $true
$lstComputers.Enabled = $true
})
# Convertir lista de resultados a DataTable para el grid
function ConvertTo-DataTable {
param([Parameter(Mandatory, ValueFromPipeline)][PSObject]$InputObject)
begin {
$dt = New-Object System.Data.DataTable
$first = $true
}
process {
$obj = $_
if ($first) {
foreach ($prop in $obj.PSObject.Properties.Name) {
[void]$dt.Columns.Add($prop)
}
$first = $false
}
$row = $dt.NewRow()
foreach ($prop in $obj.PSObject.Properties.Name) {
$row[$prop] = $obj.$prop
}
[void]$dt.Rows.Add($row)
}
end {
return $dt
}
}
function Test-ComputerOnline {
param(
[Parameter(Mandatory)][string]$ComputerName
)
try {
return Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction SilentlyContinue
}
catch {
return $false
}
}
# Evento: Buscar software en equipos seleccionados
$btnSearch.Add_Click({
$searchText = $txtSearch.Text.Trim()
if ([string]::IsNullOrWhiteSpace($searchText)) {
[System.Windows.Forms.MessageBox]::Show('Escribe el nombre (o parte) del software a buscar.', 'Aviso', 'OK',
'Information') | Out-Null
return
}
if ($lstComputers.SelectedItems.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show('Selecciona al menos un equipo de la lista.', 'Aviso', 'OK', 'Information') |
Out-Null
return
}
$selectedCount = $lstComputers.SelectedItems.Count
if ($selectedCount -gt 20) {
$answer = [System.Windows.Forms.MessageBox]::Show(
"Has seleccionado $selectedCount equipos. La búsqueda puede tardar varios minutos. ¿Deseas continuar?",
'Confirmación',
[System.Windows.Forms.MessageBoxButtons]::YesNo,
[System.Windows.Forms.MessageBoxIcon]::Question
)
if ($answer -ne [System.Windows.Forms.DialogResult]::Yes) {
return
}
}
$lblStatus.Text = 'Buscando en equipos seleccionados...'
$form.Refresh()
$btnSearch.Enabled = $false
$btnLoadComputers.Enabled = $false
$lstComputers.Enabled = $false
$progressBar.Style = 'Continuous'
$progressBar.Minimum = 0
$progressBar.Maximum = $lstComputers.SelectedItems.Count
$progressBar.Value = 0
$progressBar.Visible = $true
$useWin32 = $chkUseWin32Product.Checked
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath 'BuscarSoftwareRedModule.psm1'
if (-not (Test-Path $modulePath)) {
[System.Windows.Forms.MessageBox]::Show("No se encontró el módulo de lógica: $modulePath", 'Error', 'OK', 'Error') |
Out-Null
$btnSearch.Enabled = $true
$btnLoadComputers.Enabled = $true
$lstComputers.Enabled = $true
return
}
$jobs = @()
$offlineComputers = @()
foreach ($item in $lstComputers.SelectedItems) {
$computerName = [string]$item
if (-not (Test-ComputerOnline -ComputerName $computerName)) {
Write-BuscarSoftwareLog -Message "Equipo no accesible por ICMP (posible problema de DNS, apagado o firewall ICMP):
$computerName"
$offlineComputers += $computerName
continue
}
$lblStatus.Text = "Creando tarea para $computerName..."
$form.Refresh()
$job = Start-Job -ScriptBlock {
param($ComputerNameInner, $SearchTextInner, $UseWin32Inner, $ModulePathInner)
Import-Module $ModulePathInner -ErrorAction Stop
if ($UseWin32Inner) {
Get-InstalledSoftwareRemote -ComputerName $ComputerNameInner -SearchText $SearchTextInner -UseWin32Product
}
else {
Get-InstalledSoftwareRemote -ComputerName $ComputerNameInner -SearchText $SearchTextInner -UseWin32Product:$false
}
} -ArgumentList $computerName, $searchText, $useWin32, $modulePath
$jobs += $job
}
$results = @()
foreach ($job in $jobs) {
$lblStatus.Text = "Esperando resultados de tareas en segundo plano..."
$form.Refresh()
$null = Wait-Job -Job $job -Timeout 300
$res = Receive-Job -Job $job -ErrorAction SilentlyContinue
if ($res) {
$results += $res
}
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue | Out-Null
if ($progressBar.Value -lt $progressBar.Maximum) {
$progressBar.Value += 1
}
}
if ($results.Count -eq 0) {
$lblStatus.Text = 'Búsqueda finalizada. No se encontraron coincidencias.'
$grid.DataSource = $null
}
else {
$lblStatus.Text = "Búsqueda finalizada. Resultados: $($results.Count)"
# Construir DataTable explícito con normalización de InstallDate
$dt = New-Object System.Data.DataTable
[void]$dt.Columns.Add('ComputerName')
[void]$dt.Columns.Add('DisplayName')
[void]$dt.Columns.Add('DisplayVersion')
[void]$dt.Columns.Add('Publisher')
[void]$dt.Columns.Add('InstallDate')
foreach ($r in $results) {
$installDateText = ''
if ($null -ne $r.InstallDate -and $r.InstallDate -ne '') {
try {
$dtParsed = [datetime]$r.InstallDate
$installDateText = $dtParsed.ToString('yyyy-MM-dd')
}
catch {
$installDateText = [string]$r.InstallDate
}
}
$row = $dt.NewRow()
$row['ComputerName'] = [string]$r.ComputerName
$row['DisplayName'] = [string]$r.DisplayName
$row['DisplayVersion'] = [string]$r.DisplayVersion
$row['Publisher'] = [string]$r.Publisher
$row['InstallDate'] = $installDateText
[void]$dt.Rows.Add($row)
}
$grid.DataSource = $null
$grid.DataSource = $dt
}
$progressBar.Visible = $false
$progressBar.Value = 0
if ($offlineComputers.Count -gt 0) {
$mensajeOffline = "Los siguientes equipos no están accesibles (offline o sin respuesta ICMP):`r`n`r`n" +
($offlineComputers -join "`r`n")
[System.Windows.Forms.MessageBox]::Show($mensajeOffline, 'Equipos no accesibles', 'OK', 'Warning') | Out-Null
}
$btnSearch.Enabled = $true
$btnLoadComputers.Enabled = $true
$lstComputers.Enabled = $true
})
function Copy-CurrentGridRowToClipboard {
if (-not $grid.CurrentRow) { return }
$values = @()
foreach ($cell in $grid.CurrentRow.Cells) {
$values += [string]$cell.Value
}
$text = [string]::Join("`t", $values)
[System.Windows.Forms.Clipboard]::SetText($text)
}
$btnExportCsv.Add_Click({
if (-not $grid.DataSource -or -not $grid.Rows.Count) {
[System.Windows.Forms.MessageBox]::Show('No hay resultados para exportar.', 'Aviso', 'OK', 'Information') | Out-Null
return
}
$dialog = New-Object System.Windows.Forms.SaveFileDialog
$dialog.Filter = 'CSV (*.csv)|*.csv|Todos los archivos (*.*)|*.*'
$dialog.Title = 'Guardar resultados como CSV'
$dialog.FileName = 'Resultados_BuscarSoftware.csv'
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$path = $dialog.FileName
$dataTable = [System.Data.DataTable]$grid.DataSource
$dataTable | Export-Csv -Path $path -NoTypeInformation -Encoding UTF8
[System.Windows.Forms.MessageBox]::Show("Resultados exportados a: $path", 'Información', 'OK', 'Information') | Out-Null
}
})
$btnCopySelected.Add_Click({
if (-not $grid.CurrentRow) {
[System.Windows.Forms.MessageBox]::Show('No hay ninguna fila seleccionada.', 'Aviso', 'OK', 'Information') | Out-Null
return
}
Copy-CurrentGridRowToClipboard
[System.Windows.Forms.MessageBox]::Show('Datos de la fila copiados al portapapeles.', 'Información', 'OK',
'Information') | Out-Null
})
$btnBrowseInstaller.Add_Click({
$dialog = New-Object System.Windows.Forms.OpenFileDialog
$dialog.Filter = 'Instaladores (*.exe;*.msi)|*.exe;*.msi|Todos los archivos (*.*)|*.*'
$dialog.Title = 'Seleccionar instalador'
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$txtInstallerPath.Text = $dialog.FileName
}
})
$btnInstall.Add_Click({
if ($lstComputers.SelectedItems.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show('Selecciona al menos un equipo para instalar el software.', 'Aviso', 'OK',
'Information') | Out-Null
return
}
$installerPath = $txtInstallerPath.Text.Trim()
if ([string]::IsNullOrWhiteSpace($installerPath)) {
[System.Windows.Forms.MessageBox]::Show('Selecciona o escribe la ruta del instalador.', 'Aviso', 'OK', 'Information') |
Out-Null
return
}
$silentArgs = $txtInstallArgs.Text.Trim()
$customCommand = $null
$expectedDisplayName = $txtExpectedName.Text.Trim()
if ([string]::IsNullOrWhiteSpace($expectedDisplayName)) {
$expectedDisplayName = $txtSearch.Text.Trim()
}
$txtExpectedName.Text = $expectedDisplayName
$msg = "Se intentara instalar el software de forma desatendida en los equipos seleccionados.\n\n" +
"Instalador: $installerPath\n" +
"Parametros: $silentArgs\n\n" +
"Nota: Se probaran varios metodos (Invoke-Command, copia remota, WMI)."
$answer = [System.Windows.Forms.MessageBox]::Show($msg, 'Confirmar instalación',
[System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question)
if ($answer -ne [System.Windows.Forms.DialogResult]::Yes) { return }
$btnInstall.Enabled = $false
$btnSearch.Enabled = $false
$btnLoadComputers.Enabled = $false
$lstComputers.Enabled = $false
$lblStatus.Text = 'Iniciando instalación desatendida en equipos seleccionados...'
$form.Refresh()
$progressBar.Style = 'Continuous'
$progressBar.Minimum = 0
$progressBar.Maximum = $lstComputers.SelectedItems.Count
$progressBar.Value = 0
$progressBar.Visible = $true
$ok = 0
$fail = 0
foreach ($item in $lstComputers.SelectedItems) {
$computerName = [string]$item
$lblStatus.Text = "Instalando en $computerName..."
$form.Refresh()
$result = Invoke-RemoteInstall -ComputerName $computerName -InstallerPath $installerPath -SilentArgs $silentArgs
-CustomCommand $customCommand -ExpectedDisplayName $expectedDisplayName
if ($result) { $ok++ } else { $fail++ }
if ($progressBarInstall.Value -lt $progressBarInstall.Maximum) {
$progressBarInstall.Value += 1
}
}
$progressBarInstall.Visible = $false
$progressBarInstall.Value = 0
$lblStatus.Text = "Instalacion finalizada. Exitosas: $ok. Fallidas: $fail."
$btnInstall.Enabled = $true
$btnSearch.Enabled = $true
$btnLoadComputers.Enabled = $true
$lstComputers.Enabled = $true
[System.Windows.Forms.MessageBox]::Show("Instalacion finalizada. Exitosas: $ok. Fallidas: $fail.", 'Resultado de
instalación', 'OK', 'Information') | Out-Null
})
$btnUninstall.Add_Click({
if (-not $grid.DataSource -or -not $grid.Rows.Count) {
[System.Windows.Forms.MessageBox]::Show('No hay resultados en la tabla de software para desinstalar.', 'Aviso', 'OK',
'Information') | Out-Null
return
}
if ($grid.SelectedRows.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show('Selecciona al menos una fila en la tabla de resultados para desinstalar.',
'Aviso', 'OK', 'Information') | Out-Null
return
}
$count = $grid.SelectedRows.Count
$msg = "Se intentara desinstalar de forma desatendida el software de las filas seleccionadas.\n\n" +
"Filas seleccionadas: $count\n\n" +
"Se usaran metodos en cascada (QuietUninstallString, UninstallString, Win32_Product)."
$answer = [System.Windows.Forms.MessageBox]::Show($msg, 'Confirmar desinstalación',
[System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Warning)
if ($answer -ne [System.Windows.Forms.DialogResult]::Yes) { return }
$btnUninstall.Enabled = $false
$btnInstall.Enabled = $false
$btnSearch.Enabled = $false
$btnLoadComputers.Enabled = $false
$lstComputers.Enabled = $false
$lblStatus.Text = 'Iniciando desinstalación en equipos seleccionados...'
$form.Refresh()
$progressBarInstall.Style = 'Continuous'
$progressBarInstall.Minimum = 0
$progressBarInstall.Maximum = $grid.SelectedRows.Count
$progressBarInstall.Value = 0
$progressBarInstall.Visible = $true
$ok = 0
$fail = 0
foreach ($row in $grid.SelectedRows) {
$computerName = [string]$row.Cells['ComputerName'].Value
$displayName = [string]$row.Cells['DisplayName'].Value
if ([string]::IsNullOrWhiteSpace($computerName) -or [string]::IsNullOrWhiteSpace($displayName)) {
continue
}
$lblStatus.Text = "Desinstalando '$displayName' en $computerName..."
$form.Refresh()
$result = Invoke-RemoteUninstall -ComputerName $computerName -DisplayName $displayName
if ($result) { $ok++ } else { $fail++ }
if ($progressBar.Value -lt $progressBar.Maximum) {
$progressBar.Value += 1
}
}
$progressBar.Visible = $false
$progressBar.Value = 0
$lblStatus.Text = "Desinstalacion finalizada. Exitosas: $ok. Fallidas: $fail."
$btnUninstall.Enabled = $true
$btnInstall.Enabled = $true
$btnSearch.Enabled = $true
$btnLoadComputers.Enabled = $true
$lstComputers.Enabled = $true
[System.Windows.Forms.MessageBox]::Show("Desinstalacion finalizada. Exitosas: $ok. Fallidas: $fail.", 'Resultado de
desinstalación', 'OK', 'Information') | Out-Null
})
$grid.Add_CellDoubleClick({
param($sender, $e)
if ($e.RowIndex -ge 0) {
Copy-CurrentGridRowToClipboard
}
})
[void]$form.ShowDialog()
Módulo BuscarSoftwareRedModule.psm1
Este módulo contiene las funciones reutilizables para obtener equipos del dominio y consultar software instalado remotamente. Aquí se muestra su contenido completo.
#requires -Version 5.1
function Get-DomainComputers {
param(
[ValidateSet('All', 'Servers', 'Workstations')]
[string]$Type = 'All'
)
try {
Import-Module ActiveDirectory -ErrorAction Stop
}
catch {
throw "No se pudo importar el módulo ActiveDirectory. Asegúrate de tener RSAT/AD instalado. Detalle: $_"
}
try {
$ldapFilter = '(objectClass=computer)'
switch ($Type) {
'Servers' {
$ldapFilter = '(operatingSystem=*Server*)'
}
'Workstations' {
$ldapFilter = '(!(operatingSystem=*Server*))'
}
}
$computers = Get-ADComputer -LDAPFilter $ldapFilter -Properties OperatingSystem |
Select-Object Name, OperatingSystem
return $computers | Sort-Object Name
}
catch {
throw "Error al obtener equipos del dominio: $_"
}
}
function Get-RemoteSoftwareFromRegistry {
param(
[Parameter(Mandatory)][string]$ComputerName,
[Parameter(Mandatory)][string]$SearchText
)
$scriptBlock = {
param($SearchTextInner)
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$apps = foreach ($path in $paths) {
Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and ($_.DisplayName -like "*" + $SearchTextInner + "*") } |
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate
}
return $apps
}
try {
$sessionOptions = New-PSSessionOption -OperationTimeout 120000
$result = Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $SearchText -SessionOption $sessionOptions -ErrorAction Stop
foreach ($app in $result) {
[PSCustomObject]@{
ComputerName = $ComputerName
DisplayName = $app.DisplayName
DisplayVersion = $app.DisplayVersion
Publisher = $app.Publisher
InstallDate = $app.InstallDate
}
}
}
catch {
@()
}
}
function Get-RemoteSoftwareFromPackage {
param(
[Parameter(Mandatory)][string]$ComputerName,
[Parameter(Mandatory)][string]$SearchText
)
$scriptBlock = {
param($SearchTextInner)
try {
$packages = Get-Package -ErrorAction Stop |
Where-Object { $_.Name -and ($_.Name -like "*" + $SearchTextInner + "*") }
foreach ($pkg in $packages) {
[PSCustomObject]@{
DisplayName = $pkg.Name
DisplayVersion = $pkg.Version
Publisher = $pkg.ProviderName
InstallDate = $null
}
}
}
catch {
@()
}
}
try {
$sessionOptions = New-PSSessionOption -OperationTimeout 120000
$result = Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ArgumentList $SearchText -SessionOption $sessionOptions -ErrorAction Stop
foreach ($app in $result) {
[PSCustomObject]@{
ComputerName = $ComputerName
DisplayName = $app.DisplayName
DisplayVersion = $app.DisplayVersion
Publisher = $app.Publisher
InstallDate = $app.InstallDate
}
}
}
catch {
@()
}
}
function Get-RemoteSoftwareFromWmi {
param(
[Parameter(Mandatory)][string]$ComputerName,
[Parameter(Mandatory)][string]$SearchText
)
try {
$instances = Get-CimInstance -ClassName Win32_Product -ComputerName $ComputerName -OperationTimeoutSec 120 -ErrorAction Stop |
Where-Object { $_.Name -and ($_.Name -like "*" + $SearchText + "*") }
foreach ($inst in $instances) {
[PSCustomObject]@{
ComputerName = $ComputerName
DisplayName = $inst.Name
DisplayVersion = $inst.Version
Publisher = $inst.Vendor
InstallDate = $inst.InstallDate
}
}
}
catch {
@()
}
}
function Get-InstalledSoftwareRemote {
param(
[Parameter(Mandatory)] [string]$ComputerName,
[Parameter(Mandatory)] [string]$SearchText,
[switch]$UseWin32Product = $true
)
if (-not (Test-ComputerOnline -ComputerName $ComputerName)) {
return @()
}
$results = @()
# 1) Registro (método principal)
$results += Get-RemoteSoftwareFromRegistry -ComputerName $ComputerName -SearchText $SearchText
# 2) Get-Package como fallback si no hubo resultados
if (-not $results -or $results.Count -eq 0) {
$results += Get-RemoteSoftwareFromPackage -ComputerName $ComputerName -SearchText $SearchText
}
# 3) WMI (Win32_Product) como último recurso automático si no hay resultados
if ($UseWin32Product -and (-not $results -or $results.Count -eq 0)) {
$results += Get-RemoteSoftwareFromWmi -ComputerName $ComputerName -SearchText $SearchText
}
if (-not $results -or $results.Count -eq 0) {
return @()
}
return $results
}
function Test-ComputerOnline {
param(
[Parameter(Mandatory)][string]$ComputerName
)
try {
# ICMP + resolución DNS; -Quiet devuelve True/False
return Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction SilentlyContinue
}
catch {
return $false
}
}