Ciberseguridad y seguridad de la información

KQL para principiantes: Primeras consultas para la investigación de incidentes

6 min de lecturaPublicado: 5 de agosto de 2026
Ilustración visual profesional sobre KQL para principiantes en SIEM y detección
Respuesta rápida

KQL — Kusto Query Language — es un lenguaje de consulta para leer y analizar datos en productos como Azure Monitor y Microsoft Sentinel. Una consulta generalmente comienza con una tabla y continúa con una tubería de comandos: filtran tiempo y eventos, seleccionan o crean campos, resumen por usuario o activo y muestran los resultados relevantes para la investigación. La clave para aprender es comenzar con una pregunta y construir la consulta paso a paso.

Un analista de SOC no necesita memorizar cientos de comandos para empezar a trabajar con KQL. Necesita entender el modelo de pensamiento: en qué tabla se encuentra la información, cuál es el rango de tiempo, qué filas son relevantes, qué campos se necesitan y cómo resumir los resultados para responder a una pregunta de investigación.

KQL es un lenguaje para leer y analizar. No es SQL, aunque existen conceptos similares. Los datos fluyen de izquierda a derecha a través de un Pipe — el signo | — y cada línea recibe el resultado de la línea anterior. Así se puede construir una búsqueda pequeña, verificar un resultado y añadir un paso adicional sin escribir todo de una vez.

Los ejemplos de la guía utilizan nombres de tablas y campos comunes, pero el Schema varía entre Workspaces y fuentes. Antes de copiar una Query, abran algunas entradas, verifiquen los campos reales y ajusten la lógica. Todos los datos del ejercicio son simulados.

Estructura básica de una Query

La primera línea suele indicar una Table. Cada Pipe añade un Operator. Por ejemplo, una Query que muestra las diez últimas conexiones de la tabla SigninLogs:

SigninLogs
| where TimeGenerated > ago(24h)
| project TimeGenerated, UserPrincipalName, IPAddress, ResultType
| sort by TimeGenerated desc
| take 10

Lean la consulta como una frase: toma SigninLogs, guarda solo los eventos de las últimas 24 horas, muestra cuatro campos, ordena de más reciente a más antiguo y toma diez filas. El orden de los pasos es importante tanto para la comprensión como para el rendimiento. El filtrado temprano reduce la cantidad de datos que pasan a las siguientes etapas.

Paso 1: Conozcan la tabla

Antes de investigar, ejecuten algunas filas para ver el Schema y ejemplos. El comando take es excelente para el aprendizaje, pero no garantiza los últimos eventos a menos que se ordenen. Se puede usar project para reducir la vista y project-away para ocultar campos innecesarios.

SigninLogs
| take 5

Presten atención a los campos de tipo datetime, string, dynamic e int. Un campo dynamic puede contener JSON o un array y a veces requiere parse_json, mv-expand o acceso a una propiedad interna. No asuman que ResultType es el mismo para cada fuente. En la documentación de la tabla o en ejemplos reales, verifiquen el significado de los valores.

Paso 2: Filtrado mediante where

where es la herramienta de investigación central. Se puede filtrar por tiempo, usuario, IP, resultado o texto. Es preferible comenzar con un rango de tiempo estrecho y usar comparaciones que se ajusten al tipo de campo.

SigninLogs
| where TimeGenerated between (datetime(2026-08-01 08:00:00) .. datetime(2026-08-01 12:00:00))
| where UserPrincipalName =~ "student@contoso.example"
| project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, ResultType

El operador =~ compara una cadena sin distinción de mayúsculas y minúsculas. Para búsquedas parciales se pueden usar contains, has o startswith. En muchos casos, has es más eficiente y preciso para buscar un término completo. Eviten el filtrado muy amplio con contains cuando se puede usar un campo estructurado.

Paso 3: Selección de campos y creación de contexto

project devuelve solo los campos seleccionados. extend crea un nuevo campo sin eliminar los existentes. Esto es útil para etiquetas, cálculos y normalización local.

SigninLogs
| where TimeGenerated > ago(24h)
| extend Outcome = iff(tostring(ResultType) == "0", "Success", "Failure")
| project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, Outcome

Durante la investigación, prefieran nombres de campos que aclaren el significado. Se puede usar project-rename para cambiar un nombre para visualización, pero no oculten la fuente de los datos. Un Ticket profesional debe indicar de qué Table y campo proviene cada hallazgo importante.

Paso 4: summarize — transformar eventos en una imagen

summarize agrupa eventos y calcula Aggregations. Se puede contar, encontrar el primer y último tiempo, recopilar valores, calcular el número de usuarios únicos y construir una Baseline. BY define los grupos.

SigninLogs
| where TimeGenerated > ago(24h)
| summarize Attempts=count(),
            FirstSeen=min(TimeGenerated),
            LastSeen=max(TimeGenerated),
            Applications=make_set(AppDisplayName, 10)
  by UserPrincipalName, IPAddress
| sort by Attempts desc

make_set es útil para mostrar diferentes valores, pero las listas grandes son pesadas y dificultan la lectura. Limiten el número de elementos. dcount proporciona un recuento aproximado de valores únicos y es adecuado para análisis amplios; distinct devuelve combinaciones únicas cuando es necesario verlas.

countif y dcountif

En las investigaciones de Authentication a veces se desea contar éxitos y fallos en el mismo grupo. countif cuenta solo las filas que cumplen una condición:

SigninLogs
| where TimeGenerated > ago(24h)
| summarize Failures=countif(tostring(ResultType) != "0"),
            Successes=countif(tostring(ResultType) == "0"),
            UniqueUsers=dcount(UserPrincipalName)
  by IPAddress
| where Failures >= 5
| sort by Failures desc

El resultado es un Lead. No demuestra que el éxito ocurrió después de los fallos, y una IP compartida puede ser NAT, Proxy o VPN. Se deben abrir los eventos brutos y construir una Timeline antes de la Classification.

Ventanas de tiempo mediante bin

bin agrupa datetime en segmentos fijos. Esto permite ver un Burst de actividad en lugar de resumir un día completo. Elijan Span según el comportamiento: un Password Spray puede extenderse en el tiempo para evitar un umbral; un Brute Force contra una sola cuenta puede ser rápido.

SigninLogs
| where TimeGenerated > ago(24h)
| summarize Failures=countif(tostring(ResultType) != "0"),
            Successes=countif(tostring(ResultType) == "0"),
            Users=dcount(UserPrincipalName)
  by IPAddress, bin(TimeGenerated, 30m)
| where Failures >= 10 and Successes >= 1
| sort by TimeGenerated desc

La columna TimeGenerated en el resultado representa el inicio del Bin. Para ver el orden exacto, usen el resultado para seleccionar una IP y una ventana, y luego ejecuten una segunda Query que muestre los eventos por tiempo.

let — hacer que la Query sea legible

let permite dar un nombre a un valor o a una tabla temporal. Es útil para definir un Time range, Threshold o un grupo de datos que se usa varias veces.

let Lookback = 24h;
let FailureThreshold = 10;
SigninLogs
| where TimeGenerated > ago(Lookback)
| summarize Failures=countif(tostring(ResultType) != "0"),
            Successes=countif(tostring(ResultType) == "0"),
            Users=dcount(UserPrincipalName),
            UserList=make_set(UserPrincipalName, 20)
  by IPAddress, bin(TimeGenerated, 30m)
| where Failures >= FailureThreshold and Successes >= 1
| project TimeGenerated, IPAddress, Failures, Successes, Users, UserList
| order by Failures desc

Nombres claros y comentarios reducen los errores. En KQL se pueden añadir comentarios usando //. Una Query diseñada para convertirse en una Analytics rule debe describir el propósito de la detección, las suposiciones, las fuentes, la versión y el Owner.

Ejercicio práctico: Fallos seguidos de Login exitoso

En el laboratorio, ejecuten la Query de resumen sobre datos simulados. Elijan una fila donde existan Failures y Successes. Luego realicen un Drill-down:

let TargetIP = "203.0.113.25";
let WindowStart = datetime(2026-08-01 09:00:00);
SigninLogs
| where TimeGenerated between (WindowStart .. WindowStart + 30m)
| where IPAddress == TargetIP
| extend Outcome = iff(tostring(ResultType) == "0", "Success", "Failure")
| project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, Outcome, ResultDescription
| order by TimeGenerated asc

Respondan a estas preguntas: ¿Los fallos afectaron a uno o muchos usuarios? ¿El éxito pertenece al mismo usuario? ¿La IP es conocida como VPN? ¿Se activó MFA? ¿Existe actividad adicional después del éxito? Solo después de recopilar el contexto se puede determinar si se trata de un Password Spray, un error de usuario, un servicio antiguo o una actividad aprobada.

Optimización y seguridad en el trabajo

  • Filtren el tiempo temprano y usen la tabla más precisa.
  • Filtren en campos estructurados en lugar de realizar una búsqueda de texto sobre Raw data.
  • Muestren solo los campos necesarios usando project.
  • Eviten join en grandes volúmenes antes de verificar alternativas como lookup o summarize.
  • Limiten make_set y make_list.
  • Prueben la Query en un rango pequeño antes de expandirla.
  • Documenten suposiciones, Thresholds y el significado de los valores de Result.
  • No conviertan una Query en una Detection antes de verificar True/False/Benign Positives.

Errores comunes

  • Copiar una Query sin verificar el Schema local.
  • Resumir un día completo y deducir que hubo una secuencia de eventos.
  • Olvidar el rango de tiempo y escanear un gran volumen innecesariamente.
  • Usar un nombre de campo diferente entre dos fuentes como si fuera uniforme.
  • Tratar el resultado de una Query como prueba de un ataque.
  • Crear listas enormes usando make_set.
  • Escribir una Query larga de una sola vez en lugar de verificar cada paso.

Resumen y CTA

Abran un entorno de Log Analytics o un laboratorio con datos simulados y construyan una Query en tres pasos: muestren cinco filas, filtren un evento y resuman por usuario o IP. Guarden cada versión y expliquen qué responde. Luego, pasen a la guía de investigación de Incident en Microsoft Sentinel para ver cómo la Query se integra en un Case real.

Preguntas frecuentes

¿KQL es similar a SQL?

Existen conceptos comunes como filtrado, Projection y Aggregation, pero la sintaxis y el modelo de Pipe son diferentes. Es recomendable aprender KQL como un lenguaje en sí mismo.

¿Cuál es la diferencia entre where y search?

where filtra por expresiones y campos definidos y es preferido en la mayoría de las investigaciones. search puede buscar valores de forma más amplia, pero a veces es menos preciso y eficiente.

¿Puede una Query modificar o eliminar logs?

KQL en el contexto de Log Analytics y Sentinel se utiliza para lectura y análisis. Las operaciones de administración e Ingestion se realizan mediante otros mecanismos y permisos adecuados.

¿Cuál es la diferencia entre summarize y distinct?

distinct devuelve combinaciones únicas. summarize agrupa y calcula valores como count, min, max o make_set.

¿Cómo saber qué Table buscar?

Se comienza con el Data connector y la documentación del Schema, se usa la búsqueda de Tables en Log Analytics y se revisan ejemplos de eventos. El mismo Use Case puede usar diferentes tablas entre organizaciones.

¿Quieres comprobar si este itinerario es para ti?

Deja tus datos y un asesor de HPI te llamará para una breve charla de orientación, sin compromiso.

Tus datos se guardan de forma segura.

Para estudios de SOC y ciberseguridad dentro del programa Cybersecurity & AI

¿Quieres los detalles del programa? Déjanos tus datos y te contactaremos.

Artículos relacionados