<# .SYNOPSIS Ejecuta una consulta KQL de búsqueda avanzada de Microsoft Defender XDR y exporta el resultado. .DESCRIPTION Manda la consulta que escribas en -Consulta o guardes en un archivo .kql a runHuntingQuery y muestra o exporta las filas con las columnas que la consulta devuelva, sin traducirlas. Las fechas salen en UTC, como las entrega la API. Solo lectura: el POST es una consulta y KQL no modifica datos del tenant. .PARAMETER Consulta Texto de la consulta KQL. .PARAMETER RutaKql Archivo con la consulta KQL. .PARAMETER Dias Días hacia atrás (se envía como Timespan). Si se omite, la API usa 30. Los datos propios de Defender XDR solo llegan a 30 días; más atrás solo aplica con Microsoft Sentinel conectado. .PARAMETER RutaCsv Ruta del CSV de salida. Si se omite, solo se muestra en pantalla. .EXAMPLE .\invoke-consulta-hunting.ps1 -RutaKql .\consulta.kql -RutaCsv .\resultado.csv .EXAMPLE .\invoke-consulta-hunting.ps1 -Consulta 'DeviceInfo | summarize count() by OSPlatform' -Dias 7 .NOTES Permisos: ThreatHunting.Read.All Rol mínimo: Lector de seguridad Licencia: Búsqueda avanzada de Microsoft Defender XDR: Microsoft Defender for Endpoint Plan 2 (tablas Device*) o Microsoft Defender for Office 365 Plan 2 (tablas Email*, UrlClickEvents), incluidos en Microsoft 365 E5. Microsoft 365 Business Premium no la incluye sin Microsoft Defender Suite for Business Premium Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas. #> [CmdletBinding(DefaultParameterSetName = 'Texto')] param( [Parameter(Mandatory, ParameterSetName = 'Texto')] [string]$Consulta, [Parameter(Mandatory, ParameterSetName = 'Archivo')] [string]$RutaKql, [ValidateRange(1, 365)] [int]$Dias, [string]$RutaCsv ) $ErrorActionPreference = 'Stop' $permisos = @('ThreatHunting.Read.All') if ($RutaKql) { $Consulta = Get-Content -Path $RutaKql -Raw -Encoding utf8 } if ([string]::IsNullOrWhiteSpace($Consulta)) { throw 'La consulta está vacía.' } 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 ConvertirCelda($valor) { if ($valor -is [string]) { # Un remitente o asunto puede traer una fórmula: el apóstrofo evita que Excel la ejecute. if ($valor -match '^[=+\-@\t\r]') { return "'$valor" } return $valor } if ($valor -is [System.Collections.IEnumerable]) { return ConvertTo-Json -InputObject $valor -Compress -Depth 10 } $valor } $cuerpo = @{ Query = $Consulta } if ($Dias) { $cuerpo.Timespan = "P$($Dias)D" } $respuesta = Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/v1.0/security/runHuntingQuery' -Body ($cuerpo | 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.' } $columnas = @($respuesta.schema | ForEach-Object { $_.name }) $filas = foreach ($r in $respuesta.results) { $fila = [ordered]@{} # Se usa el índice porque una columna llamada Count chocaría con la propiedad del hashtable. foreach ($c in $columnas) { $fila[$c] = ConvertirCelda $r[$c] } [pscustomobject]$fila } Write-Host "Filas devueltas: $(@($filas).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 }