Estado del antivirus en computadoras Windows
Revisa en cada computadora Windows de Intune si la protección en tiempo real está apagada, si las definiciones están vencidas o si falta un análisis.
Ejemplo en México
Cadena de farmacias en Guadalajara, 14 sucursales
Las computadoras de sucursal casi no se reinician y varias pasan semanas sin conexión estable. Nadie sabe cuáles siguen protegidas.
.\get-estado-antivirus.ps1 -SoloConProblemas -RutaCsv .\antivirus.csvObtiene las computadoras con definiciones vencidas o tiempo real apagado y programa la visita a esas sucursales.
Cómo implementarlo
- Instala el módulo si no lo tienes: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser.
- Corre el script con -SoloConProblemas e inicia sesión con una cuenta con el rol Read Only Operator de Intune.
- Filtra en el CSV la columna Problemas y empieza por Tiempo real apagado.
- Corrige esas computadoras y lanza un análisis con start-analisis-antivirus.
Ver el código completo de get-estado-antivirus.ps1
<#
.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 ', ')" }