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.
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.csvObtiene los usuarios que entraron a la liga y les restablece la contraseña ese mismo día.
Cómo implementarlo
- Instala el módulo si no lo tienes: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser.
- Corre el script con -Dias e inicia sesión con una cuenta con el rol Lector de seguridad.
- Filtra en el CSV la columna Resultado por Entró a la liga.
- 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
}