Public/Get-TechCareDiagnostics.ps1

function Get-TechCareDiagnostics {
    <#
    .SYNOPSIS
        Recopila el diagnostico del equipo en modo solo lectura.
    .DESCRIPTION
        Reune sistema operativo, CPU, memoria, discos, salud fisica de los discos,
        tiempo encendido, errores criticos recientes, actualizaciones pendientes y
        programas de inicio. No modifica nada, por lo que no declara ShouldProcess
        ni requiere privilegios de administrador; sin elevacion simplemente hay
        apartados que el sistema no deja leer, y eso se refleja en los hallazgos.
    .EXAMPLE
        Get-TechCareDiagnostics

        Diagnostico completo del equipo local.
    .EXAMPLE
        (Get-TechCareDiagnostics).Recommendations

        Solo las recomendaciones, para pegarlas en el parte de trabajo.
    .OUTPUTS
        PSCustomObject
    .LINK
        https://github.com/earbona23/TechCare
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseSingularNouns', '',
        Justification = 'El cmdlet devuelve un lote de diagnosticos, no uno solo, y el nombre publico esta congelado por el manifiesto 0.1.0. Renombrarlo romperia a los consumidores sin aportar claridad.')]
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param()

    if (-not (Test-TechCarePlatform)) { throw $script:PlatformMessage }

    $findings = New-Object -TypeName System.Collections.ArrayList
    $recs     = New-Object -TypeName System.Collections.ArrayList
    $status   = 'Green'
    $mod      = 'Diagnose'

    Write-Progress -Activity 'TechCare' -Status 'Diagnostico del equipo' -PercentComplete 5
    Write-TechCareLog -ModuleName $mod -Message 'Iniciando diagnostico (solo lectura).'

    try {
        $os  = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
        $cs  = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
        $cpu = Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop | Select-Object -First 1

        [void]$findings.Add(('Equipo: {0} / usuario: {1}' -f $cs.Name, $cs.UserName))
        [void]$findings.Add(('SO: {0} (build {1})' -f $os.Caption, $os.BuildNumber))
        [void]$findings.Add(('CPU: {0} - {1} nucleos / {2} hilos' -f $cpu.Name, $cpu.NumberOfCores, $cpu.NumberOfLogicalProcessors))
        # FreePhysicalMemory viene en kilobytes, de ahi la division por 1MB.
        [void]$findings.Add(('RAM: {0:N1} GB totales, {1:N1} GB libres' -f ($cs.TotalPhysicalMemory / 1GB), ($os.FreePhysicalMemory / 1MB)))

        $uptime = (Get-Date) - $os.LastBootUpTime
        [void]$findings.Add(('Tiempo encendido: {0:N1} dias' -f $uptime.TotalDays))
        if ($uptime.TotalDays -gt 30) {
            $status = 'Yellow'
            [void]$recs.Add('Mas de 30 dias sin reiniciar: programar un reinicio planificado. TechCare no reinicia nunca.')
        }

        $disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ErrorAction Stop
        foreach ($disk in $disks) {
            if ($disk.Size -le 0) { continue }
            $freePct = [math]::Round(($disk.FreeSpace / $disk.Size) * 100, 1)
            [void]$findings.Add(('Disco {0}: {1:N1} GB libres de {2:N1} GB ({3}%)' -f $disk.DeviceID, ($disk.FreeSpace / 1GB), ($disk.Size / 1GB), $freePct))
            if ($freePct -lt 10) {
                $status = 'Red'
                [void]$recs.Add("Disco $($disk.DeviceID) por debajo del 10% libre.")
            } elseif ($freePct -lt 20) {
                if ($status -ne 'Red') { $status = 'Yellow' }
                [void]$recs.Add("Disco $($disk.DeviceID) con menos del 20% libre.")
            }
        }
    } catch {
        $status = 'Yellow'
        [void]$findings.Add('No se pudo completar la recopilacion basica: ' + $_.Exception.Message)
        Write-TechCareLog -ModuleName $mod -Level Error -Message 'Fallo en recopilacion basica.' -ErrorText $_.Exception.Message
    }

    # Get-PhysicalDisk solo existe con el modulo Storage; en ediciones donde falta
    # se cae a la clase SMART de WMI, que lleva en Windows desde XP.
    try {
        $physical = Get-PhysicalDisk -ErrorAction Stop
        foreach ($pd in $physical) {
            [void]$findings.Add(('Disco fisico: {0} - salud {1} - estado {2}' -f $pd.FriendlyName, $pd.HealthStatus, ($pd.OperationalStatus -join ',')))
            if ($pd.HealthStatus -ne 'Healthy') {
                $status = 'Red'
                [void]$recs.Add("Disco fisico $($pd.FriendlyName) no reporta salud correcta: respaldar antes de cualquier reparacion.")
            }
        }
    } catch {
        try {
            $smart = Get-CimInstance -Namespace 'root\wmi' -ClassName MSStorageDriver_FailurePredictStatus -ErrorAction Stop
            foreach ($s in $smart) {
                [void]$findings.Add(('SMART: prediccion de fallo = {0}' -f $s.PredictFailure))
                if ($s.PredictFailure) {
                    $status = 'Red'
                    [void]$recs.Add('SMART predice fallo de disco: respaldar antes de cualquier reparacion.')
                }
            }
        } catch {
            [void]$findings.Add('Salud SMART no disponible en este equipo.')
        }
    }

    try {
        $events = @(Get-WinEvent -FilterHashtable @{ LogName = 'System'; Level = 1, 2; StartTime = (Get-Date).AddDays(-7) } -MaxEvents 15 -ErrorAction Stop)
        [void]$findings.Add(('Errores y criticos en System (7 dias): {0}' -f $events.Count))
        foreach ($ev in ($events | Select-Object -First 5)) {
            [void]$findings.Add((' - {0} | ID {1} | {2}' -f $ev.TimeCreated, $ev.Id, $ev.ProviderName))
        }
        if ($events.Count -ge 10) {
            if ($status -ne 'Red') { $status = 'Yellow' }
            [void]$recs.Add('Volumen alto de errores en el Visor de eventos: revisar proveedor y origen.')
        }
    } catch {
        [void]$findings.Add('Visor de eventos: sin errores recientes legibles o acceso insuficiente.')
    }

    try {
        $session  = New-Object -ComObject Microsoft.Update.Session
        $searcher = $session.CreateUpdateSearcher()
        $pending  = $searcher.Search("IsInstalled=0 and IsHidden=0 and Type='Software'")
        [void]$findings.Add(('Actualizaciones de software pendientes: {0}' -f $pending.Updates.Count))
        if ($pending.Updates.Count -gt 0) {
            if ($status -eq 'Green') { $status = 'Yellow' }
            [void]$recs.Add('Hay actualizaciones pendientes: usar el modulo Update.')
        }
    } catch {
        [void]$findings.Add('Busqueda de actualizaciones no disponible: ' + $_.Exception.Message)
    }

    try {
        $runKeys = @(
            'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run',
            'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run',
            'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
        )
        $startup = New-Object -TypeName System.Collections.ArrayList
        foreach ($key in $runKeys) {
            if (-not (Test-Path -LiteralPath $key)) { continue }
            $item = Get-ItemProperty -LiteralPath $key -ErrorAction SilentlyContinue
            if ($null -eq $item) { continue }
            # Las propiedades PS* las anade PowerShell al objeto, no son entradas
            # de arranque reales.
            $item.PSObject.Properties |
                Where-Object { $_.Name -notlike 'PS*' } |
                ForEach-Object { [void]$startup.Add($_.Name) }
        }
        [void]$findings.Add(('Programas de inicio (registro): {0}' -f $startup.Count))
        foreach ($s in $startup) { [void]$findings.Add(' - ' + $s) }
        if ($startup.Count -gt 12) {
            if ($status -eq 'Green') { $status = 'Yellow' }
            [void]$recs.Add('Muchos programas de inicio: revisar con el modulo Performance.')
        }
    } catch {
        [void]$findings.Add('No se pudo enumerar el arranque automatico: ' + $_.Exception.Message)
    }

    Write-Progress -Activity 'TechCare' -Status 'Diagnostico completado' -Completed
    Write-TechCareLog -ModuleName $mod -Level Success -Message ('Diagnostico finalizado con estado {0}.' -f $status)

    return Add-TechCareResult -Module $mod -Status $status -Findings $findings.ToArray() -Recommendations $recs.ToArray()
}