Saltar al contenido
Microsoft scripts

Scripts de Defender

Quién hizo clic en ligas maliciosas

Lista los clics en ligas que Safe Links marcó como amenaza y distingue a quien entró de quien quedó bloqueado, para revisar cuentas y capacitar.

Antes de correrloCon Microsoft 365 Business Premium no funciona solo: Defender for Office 365 Plan 1 no incluye búsqueda avanzada, aunque en el portal sí tienes Detecciones en tiempo real. Necesitas Plan 2 o Microsoft Defender Suite for Business Premium. UrlClickEvents solo registra clics protegidos por Safe Links, y el veredicto es el del momento del clic: una liga que se declaró maliciosa después no aparece aquí como amenaza.

Ejemplo en México

Agencia aduanal en Nuevo Laredo, 70 personas

Llegó una campaña de facturas falsas al cierre fiscal y hay que saber quién abrió la liga antes de que alguien entregue su contraseña.

.\get-clics-phishing.ps1 -Dias 7 -RutaCsv .\clics.csv

Obtiene los usuarios que entraron a la liga y les restablece la contraseña ese mismo día.

Cómo implementarlo

  1. Instala el módulo si no lo tienes: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser.
  2. Corre el script con -Dias e inicia sesión con una cuenta con el rol Lector de seguridad.
  3. Filtra en el CSV la columna Resultado por Entró a la liga.
  4. Restablece la contraseña y revisa los inicios de sesión de esas cuentas; a los bloqueados, solo dales capacitación.
Ver el código completo de get-clics-phishing.ps1
<#
.SYNOPSIS
    ¿Quién hizo clic en ligas maliciosas y quién llegó a entrar?

.DESCRIPTION
    Consulta UrlClickEvents con búsqueda avanzada y lista los clics en ligas que Safe Links marcó
    como amenaza, indicando si el usuario llegó a la página o quedó bloqueado. Sirve para decidir
    a quién revisarle la cuenta y a quién capacitar.
    Solo lectura: el POST a runHuntingQuery es una consulta.

.PARAMETER Dias
    Días hacia atrás, de 1 a 30 (la búsqueda avanzada guarda 30). Por defecto, 30.

.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-clics-phishing.ps1 -Dias 7 -RutaCsv .\clics.csv

.NOTES
    Permisos: ThreatHunting.Read.All
    Rol mínimo: Lector de seguridad
    Licencia: Microsoft Defender for Office 365 Plan 2 (incluido en Microsoft 365 E5 o en Microsoft Defender Suite for Business Premium). Microsoft 365 Business Premium por sí solo no alcanza: trae Plan 1, sin búsqueda avanzada
    Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas.
#>
[CmdletBinding()]
param(
    [ValidateRange(1, 30)]
    [int]$Dias = 30,
    [string]$RutaCsv,
    [string]$ZonaHoraria = 'Central Standard Time (Mexico)'
)

$ErrorActionPreference = 'Stop'
$permisos = @('ThreatHunting.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')
}
# La liga viene de un correo ajeno: el apóstrofo evita que Excel la interprete como fórmula.
function LimpiarTexto($valor) { if ("$valor" -match '^[=+\-@\t\r]') { "'$valor" } else { $valor } }

$consulta = @"
UrlClickEvents
| where Timestamp > ago($($Dias)d)
| where isnotempty(ThreatTypes)
| project Timestamp, AccountUpn, Url, ActionType, ThreatTypes, IsClickedThrough, Workload, IPAddress
| order by Timestamp desc
"@
$respuesta = Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/v1.0/security/runHuntingQuery' -Body (@{ Query = $consulta } | ConvertTo-Json) -ContentType 'application/json; charset=utf-8'
if (@($respuesta.results).Count -ge 100000) { Write-Warning 'Se alcanzó el tope de 100,000 filas de la consulta: pueden faltar resultados. Reduce el periodo o filtra más.' }

$filas = foreach ($r in $respuesta.results) {
    [pscustomobject]@{
        Fecha     = ConvertirFecha $r.Timestamp
        Usuario   = $r.AccountUpn
        # La columna llega como booleano o como 0/1 según la documentación: se aceptan ambas.
        Resultado = if ($r.ActionType -eq 'ClickAllowed' -or "$($r.IsClickedThrough)" -match '^(true|1)$') { 'Entró a la liga' } else { 'Bloqueado' }
        Amenaza   = $r.ThreatTypes
        Liga      = LimpiarTexto $r.Url
        Accion    = $r.ActionType
        Origen    = $r.Workload
        IP        = $r.IPAddress
    }
}

$entraron = @($filas | Where-Object Resultado -EQ 'Entró a la liga')
Write-Host "Clics en ligas maliciosas en $Dias días: $(@($filas).Count). Entraron a la liga: $($entraron.Count)"
$entraron | Group-Object Usuario | Sort-Object Count -Descending | ForEach-Object { Write-Host "  $($_.Name): $($_.Count)" }
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
}

Relacionados