<# .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 }