Saltar al contenido
Microsoft scripts

Scripts de Defender

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.

Antes de correrloHace una consulta por computadora, así que en tenants grandes tarda y Graph puede limitarte. El dato es el último que reportó la computadora: si tiene más de 7 días se marca como problema, y lo ajustas con -DiasSinReporte. Sin datos de protección suele indicar que el equipo no ha reportado o que usa otro antivirus. Si Graph rechaza una consulta, ese equipo sale como "No se pudo consultar" y el script termina con error.

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.csv

Obtiene las computadoras con definiciones vencidas o tiempo real apagado y programa la visita a esas sucursales.

Cómo implementarlo

  1. Instala el módulo si no lo tienes: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser.
  2. Corre el script con -SoloConProblemas e inicia sesión con una cuenta con el rol Read Only Operator de Intune.
  3. Filtra en el CSV la columna Problemas y empieza por Tiempo real apagado.
  4. 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 ', ')" }

Relacionados