<# .SYNOPSIS ¿Qué computadoras Windows tienen el antivirus apagado o con definiciones vencidas? .DESCRIPTION Recorre las computadoras Windows administradas por Intune y lee el estado de protección de cada una: protección en tiempo real, definiciones, antimalware, análisis y reinicio pendiente. Hace una consulta por dispositivo, así que tarda más en tenants grandes. Solo lectura: no cambia nada en el tenant. .PARAMETER DiasSinReporte Días sin reportar a partir de los cuales el estado se considera viejo. Por defecto, 7. .PARAMETER SoloConProblemas Deja fuera las computadoras sin problemas. .PARAMETER RutaCsv Ruta del CSV de salida. Si se omite, solo se muestra en pantalla. .PARAMETER ZonaHoraria Zona horaria para mostrar las fechas. Por defecto, Central Standard Time (Mexico). .EXAMPLE .\get-estado-antivirus.ps1 -SoloConProblemas -RutaCsv .\antivirus.csv .NOTES Permisos: DeviceManagementManagedDevices.Read.All Rol mínimo: Read Only Operator de Intune Licencia: Microsoft Intune Plan 1 (incluido en Microsoft 365 Business Premium) Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas. #> [CmdletBinding()] param( [switch]$SoloConProblemas, [ValidateRange(1, 3650)] [int]$DiasSinReporte = 7, [string]$RutaCsv, [string]$ZonaHoraria = 'Central Standard Time (Mexico)' ) $ErrorActionPreference = 'Stop' $permisos = @('DeviceManagementManagedDevices.Read.All') $zona = [TimeZoneInfo]::FindSystemTimeZoneById($ZonaHoraria) if (-not (Get-Command Connect-MgGraph -ErrorAction SilentlyContinue)) { throw 'Falta el módulo. Instálalo con: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser' } Connect-MgGraph -Scopes $permisos -NoWelcome function ConvertirFecha($valor) { if (-not $valor) { return $null } $utc = if ($valor -is [datetime]) { $valor.ToUniversalTime() } else { ([datetimeoffset]"$valor").UtcDateTime } [TimeZoneInfo]::ConvertTimeFromUtc($utc, $zona).ToString('yyyy-MM-dd HH:mm') } $uri = 'https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$select=id,deviceName,userPrincipalName,operatingSystem,lastSyncDateTime' $dispositivos = while ($uri) { $pagina = Invoke-MgGraphRequest -Method GET -Uri $uri $pagina.value $uri = $pagina.'@odata.nextLink' } $windows = @($dispositivos | Where-Object { "$($_.operatingSystem)" -like 'Windows*' }) $limiteReporte = (Get-Date).ToUniversalTime().AddDays(-$DiasSinReporte) $sinConsultar = @() $filas = foreach ($d in $windows) { $falloConsulta = $false try { $e = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$($d.id)/windowsProtectionState" } catch { $e = $null # Un 404 es un equipo sin estado; un 403 o 429 no dice nada del equipo. if ("$_" -notmatch 'NotFound|Not Found|404') { $falloConsulta = $true $sinConsultar += $d.deviceName Write-Warning "No se pudo consultar $($d.deviceName): $_" } } $reportado = if ($e.lastReportedDateTime -is [datetime]) { $e.lastReportedDateTime.ToUniversalTime() } elseif ($e.lastReportedDateTime) { ([datetimeoffset]"$($e.lastReportedDateTime)").UtcDateTime } $problemas = @( if ($falloConsulta) { 'No se pudo consultar' } elseif ($null -eq $e -or ($null -eq $e.realTimeProtectionEnabled -and $null -eq $e.signatureUpdateOverdue)) { 'Sin datos de protección' } if ($reportado -and $reportado -lt $limiteReporte) { "Sin reportar hace más de $DiasSinReporte días" } if ($e.realTimeProtectionEnabled -eq $false) { 'Tiempo real apagado' } if ($e.malwareProtectionEnabled -eq $false) { 'Antimalware apagado' } if ($e.signatureUpdateOverdue -eq $true) { 'Definiciones vencidas' } if ($e.quickScanOverdue -eq $true) { 'Análisis rápido vencido' } if ($e.fullScanRequired -eq $true) { 'Requiere análisis completo' } if ($e.rebootRequired -eq $true) { 'Reinicio pendiente' } ) if ($SoloConProblemas -and -not $problemas) { continue } [pscustomobject]@{ Dispositivo = $d.deviceName Usuario = $d.userPrincipalName Problemas = if ($problemas) { $problemas -join '; ' } else { 'Sin problemas' } TiempoReal = $e.realTimeProtectionEnabled DefinicionesViejas = $e.signatureUpdateOverdue VersionDefiniciones = $e.signatureVersion UltimoAnalisis = ConvertirFecha $e.lastQuickScanDateTime UltimoReporte = ConvertirFecha $e.lastReportedDateTime UltimaSincronizacion = ConvertirFecha $d.lastSyncDateTime } } $conProblemas = @($filas | Where-Object Problemas -NE 'Sin problemas').Count Write-Host "Computadoras Windows revisadas: $($windows.Count). Con problemas: $conProblemas" if ($RutaCsv) { # UTF-8 con BOM para que Excel en español muestre bien los acentos. $filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM Write-Host "CSV guardado en $RutaCsv" } else { $filas | Format-Table -AutoSize } # El reporte ya quedó escrito; el error avisa que está incompleto. if ($sinConsultar) { throw "Reporte incompleto, no se pudo consultar: $(@($sinConsultar) -join ', ')" }