> For the complete documentation index, see [llms.txt](https://hacking-3.gitbook.io/barre/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hacking-3.gitbook.io/barre/apuntes/blue-team/splunk/spl.md).

# SPL

### 1. Qué es SPL

**SPL (Search Processing Language)** es el lenguaje de búsqueda de Splunk. No es solo un lenguaje para “buscar texto”: es un lenguaje completo de procesamiento de datos que permite:

* Buscar eventos en índices.
* Filtrar datos por campos, tiempo, texto.
* Extraer campos de eventos crudos (`_raw`).
* Transformar eventos en tablas y series temporales.
* Calcular estadísticas y agregaciones.
* Crear dashboards y visualizaciones.
* Crear alertas y detecciones programadas.
* Desarrollar reglas de detección de seguridad (use cases).
* Hacer threat hunting exploratorio.
* Normalizar datos heterogéneos a un modelo común (CIM).
* Enriquecer eventos con lookups (listas de referencia externas).
* Correlacionar eventos entre distintas fuentes.
* Analizar series temporales (tendencias, picos, baselines).
* Optimizar búsquedas sobre grandes volúmenes de datos.

SPL está compuesto, según la documentación oficial de Splunk, por **search commands** (comandos de búsqueda), **functions** (funciones), **arguments** (argumentos) y **clauses** (cláusulas). Es decir, no es solo “una sintaxis de búsqueda de texto”, sino un lenguaje con piezas combinables como en SQL, aunque con una filosofía distinta: en SPL los datos fluyen a través de una **tubería (`|`, pipe)** de comandos, cada uno transformando el resultado del anterior.

#### Estructura básica de una búsqueda

```
index=main sourcetype=access_combined status=500
| stats º BY host
| sort - count
```

Interpretación paso a paso:

* `index=main sourcetype=access_combined status=500`: es la **búsqueda base**. Aquí Splunk decide qué eventos crudos recuperar de los índices.
* `|` (pipe): envía (canaliza) el conjunto de resultados obtenido hasta ese punto al siguiente comando. Es el equivalente conceptual a una tubería Unix (`cmd1 | cmd2`).
* `stats count BY host`: agrupa los eventos por el campo `host` y cuenta cuántos eventos hay en cada grupo. Convierte eventos individuales en una tabla agregada.
* `sort - count`: ordena la tabla resultante de mayor a menor según el campo `count` (el signo indica orden descendente).

La idea central para razonar sobre cualquier búsqueda SPL es:

```
Buscar datos → Filtrar → Crear campos → Agregar → Ordenar → Mostrar
```

***

### 2. Anatomía de una búsqueda SPL

Toda búsqueda SPL, por compleja que sea, se puede descomponer en las mismas piezas:

#### Búsqueda base

```
index=windows sourcetype=WinEventLog:Security EventCode=4624
```

Es el punto de entrada. Define qué datos se recuperan de los índices (de disco/almacenamiento) antes de aplicar cualquier procesamiento adicional.

#### Pipe

```
|
```

Encadena comandos. Cada comando recibe como entrada la salida (el conjunto de resultados) del comando anterior.

#### Comando

Ejemplos: `stats`, `eval`, `where`, `table`, `lookup`, `rex`. Un comando realiza una acción concreta sobre el conjunto de eventos/resultados que recibe.

#### Argumentos

```
count BY user
```

Son los parámetros que recibe un comando para modificar su comportamiento (en este caso, `count` es la función estadística y `BY user` indica la dimensión de agrupación).

#### Funciones

```
count(), dc(), avg(), sum(), if(), case(), coalesce()
```

Cálculos concretos que se usan dentro de comandos como `eval` o `stats`.

#### Campos

```
user, src_ip, dest_ip, host, _time, _raw
```

Son las “columnas” de cada evento. Algunos son generados automáticamente por Splunk al indexar (`_time`, `_raw`, `_indextime`…), otros provienen de la extracción de datos (`user`, `src_ip`…).

#### Alias

```
AS total
```

Permite renombrar el resultado de una función o campo calculado para que sea más legible en la salida.

#### Ejemplo completo comentado

```
index=windows sourcetype=WinEventLog:Security EventCode=4625 earliest=-24h
| stats count AS failed_logons dc(src_ip) AS unique_sources BY user
| where failed_logons > 10
| sort - failed_logons
```

* Busca eventos de login fallido (`EventCode=4625`) en las últimas 24 horas.
* Cuenta el número de fallos y el número de IPs origen distintas, por usuario.
* Filtra para quedarse solo con usuarios con más de 10 fallos.
* Ordena de mayor a menor número de fallos.

***

### 3. Comandos, funciones y operadores: diferencias clave

Es fundamental no confundir estos tres conceptos porque tienen roles distintos en una query:

#### Comando

Un **comando** ejecuta una acción sobre el conjunto de eventos/resultados. Ejemplos: `stats`, `eval`, `where`, `table`, `fields`, `lookup`, `rex`, `sort`.

```
| stats count BY user
```

Aquí `stats` es el comando.

#### Función

Una **función** calcula un valor. Se usa normalmente dentro de un comando (como `eval` o `stats`). Ejemplos: `count()`, `if()`, `case()`, `lower()`, `round()`, `coalesce()`, `dc()`, `values()`.

```
| eval user=lower(user)
```

Aquí `lower()` es la función, y `eval` es el comando que la ejecuta.

#### Operador

Un **operador** compara, combina o calcula valores entre términos. Ejemplos: `=`, `!=`, `>`, `<`, `>=`, `<=`, `AND`, `OR`, `NOT`, `+`, `-`, `*`, `/`, `%`, `.` (concatenación).

```
| where count > 10 AND status="failure"
```

Aquí `>` y `AND` son operadores.

**Regla mental**: si algo *hace* algo con todo el conjunto de resultados → comando. Si algo *calcula* un valor → función. Si algo *compara o combina* dos valores → operador.

***

### 4. Orden recomendado para optimizar una query

La regla de oro en SPL es: **filtra lo máximo posible, lo antes posible**. La documentación oficial de optimización de Splunk indica que la clave para búsquedas rápidas es limitar al mínimo los datos que se leen de disco, filtrar lo antes posible y usar la ventana temporal más pequeña posible.

#### Orden ideal de una búsqueda SPL optimizada

```
index=<index> sourcetype=<sourcetype> earliest=<time> latest=<time> field=value keyword
| fields <campos_necesarios>
| where <condiciones_complejas>
| eval <campos_calculados>
| lookup <lookup> <campo> OUTPUT <campo_enriquecido>
| stats <funciones> BY <dimensiones>
| where <filtro_post_agregacion>
| sort <campo>
| table <campos_finales>
```

> Nota: en la práctica, `eval` suele ir *antes* de `where` cuando el `where` depende de un campo calculado por `eval` (por ejemplo, filtrar por un campo normalizado). El orden exacto entre `fields`, `where` y `eval` depende de qué necesite cada paso siguiente; lo que nunca cambia es que **los filtros indexables van al principio** y **el `sort`/`table` van al final**.

#### Explicación paso a paso de por qué este orden importa

**1. Búsqueda base**

Debe incluir siempre que sea posible:

```
index=
sourcetype=
source=
host=
earliest=
latest=
```

**Ejemplo bueno:**

```
index=windows sourcetype=WinEventLog:Security EventCode=4625 earliest=-1h latest=now
```

**Ejemplo malo:**

```
*error*
```

¿Por qué es malo?

* No define índice → Splunk tiene que buscar en todos los índices posibles.
* No define sourcetype → no puede acotar el tipo de dato.
* Usa wildcard inicial (`*error*`) → obliga a un escaneo de texto libre muy costoso.
* No define ventana de tiempo → escanea potencialmente todo el histórico.

**2. Filtros indexables primero**

Campos como `index=`, `sourcetype=`, `source=`, `host=`, `_time` son “indexables”: Splunk puede usarlos para descartar buckets de datos completos sin siquiera leerlos en detalle. Ponerlos al inicio reduce drásticamente el volumen de datos que se traen desde los indexers.

**3. Keywords específicos**

**Mejor:**

```
"Failed password"
```

**Peor:**

```
*password*
```

Una keyword específica (sin wildcard) permite aprovechar el índice de términos de Splunk. Un wildcard al inicio o en medio obliga a un escaneo más costoso.

**4. Filtros simples antes que complejos**

**Mejor:**

```
index=proxy sourcetype=bluecoat action=blocked
| where bytes_out > 1000000
```

**Peor:**

```
index=proxy sourcetype=bluecoat
| where action="blocked" AND bytes_out > 1000000
```

Si `action="blocked"` se puede expresar como filtro de búsqueda base (`action=blocked`), hazlo ahí: es más eficiente que evaluarlo evento a evento con `where` después.

**5. Reducir campos pronto**

```
| fields _time user src_ip dest_ip action
```

Cuantos menos campos arrastres por la tubería, menos memoria y procesamiento se necesita en los pasos siguientes.

**6. Evitar comandos caros demasiado pronto**

Comandos como `transaction`, `join`, `sort`, `dedup`, `rex`, `spath`, `mvexpand` pueden ser costosos en términos de CPU/memoria. Úsalos solo cuando sean necesarios y, preferiblemente, después de haber filtrado el volumen de datos.

**7. Agregar lo antes posible cuando no necesites eventos crudos**

```
index=firewall action=blocked earliest=-24h
| stats count BY src_ip dest_ip dest_port
```

Es mucho más eficiente agregar pronto que arrastrar millones de eventos individuales hasta el final de la query.

**8. Usar `tstats` cuando se pueda**

El comando `tstats` realiza consultas estadísticas sobre campos indexados en archivos `.tsidx` y puede operar sobre datos indexados o modelos de datos acelerados. La documentación oficial indica que, al buscar sobre campos *index-time* en vez de eventos raw, `tstats` es más rápido que `stats`.

```
| tstats summariesonly=true count FROM datamodel=Authentication.Authentication
  WHERE Authentication.action=failure
  BY Authentication.user Authentication.src _time span=1h
```

***

### 5. Tipos de búsquedas

La documentación oficial distingue entre búsquedas de eventos crudos y búsquedas transformadoras, y además existen variantes en tiempo real y programadas.

#### 5.1 Raw event searches (búsquedas de eventos crudos)

Recuperan eventos individuales de un índice, sin transformarlos en agregaciones.

```
index=windows EventCode=4625 user="john.doe"
```

Uso típico:

* Investigación de un incidente concreto.
* Validación de una hipótesis.
* Troubleshooting.
* Revisión manual de eventos concretos.

#### 5.2 Transforming searches (búsquedas transformadoras)

Transforman eventos en estadísticas, tablas o gráficos mediante comandos como `stats`, `chart`, `timechart`.

```
index=windows EventCode=4625
| stats count BY user
```

Uso típico:

* Detecciones de seguridad.
* Dashboards.
* Alertas.
* Métricas e informes.

#### 5.3 Real-time searches (búsquedas en tiempo real)

Búsquedas que se ejecutan continuamente mostrando eventos a medida que llegan.

```
index=firewall action=blocked
```

(ejecutada en modo *real-time* desde la interfaz de Splunk).

**Recomendación:** evita el modo real-time para detecciones de seguridad salvo que exista una necesidad clara y justificada, ya que consume muchos más recursos que una búsqueda programada. Prefiere ventanas programadas con un pequeño offset respecto a “ahora”.

#### 5.4 Scheduled searches (búsquedas programadas)

Búsquedas que se ejecutan en un cron determinado, típicamente para alertas o reportes.

```
earliest=-15m@m latest=-5m@m
```

Este patrón deja un margen (`latest=-5m@m` en vez de `latest=now`) para absorber la **latencia de ingesta** (el tiempo que tardan los datos en llegar e indexarse en Splunk desde que ocurrieron). Sin este margen, una alerta podría ejecutarse antes de que todos los eventos relevantes hayan sido indexados, generando falsos negativos.

***

### 6. Tipos de comandos SPL

La documentación oficial de Splunk clasifica los comandos en **seis tipos amplios**, que no son mutuamente excluyentes (un comando puede pertenecer conceptualmente a más de una categoría según cómo se use). Entender estos tipos ayuda a comprender **dónde se procesa la data** (en los indexers o en el search head) y, por tanto, **cómo optimizar** una búsqueda.

Por ejemplo, los comandos *distributable streaming* pueden ejecutarse en los indexers (en paralelo, cerca de los datos), pero un comando como `sort` necesita reunir todos los eventos en un único lugar, lo que mueve el procesamiento al search head y puede ser más costoso.

#### 6.1 Distributable streaming

Procesan los eventos **uno a uno** y pueden ejecutarse de forma distribuida en los indexers, en paralelo.

Ejemplos: `search`, `fields`, `rename`, `eval`, `where`, `rex`, `lookup`.

**Uso recomendado:** colocarlos temprano en la query, ya que se benefician de ejecutarse cerca de los datos.

#### 6.2 Centralized streaming

Procesan los eventos uno a uno, pero de forma centralizada en el search head (no se pueden paralelizar en los indexers).

Ejemplos: `dedup`, `head`, `tail`, `streamstats`.

**Uso recomendado:** después de haber reducido el volumen de datos con filtros previos.

#### 6.3 Transforming

Transforman el conjunto de eventos en resultados agregados (tablas de estadísticas).

Ejemplos: `stats`, `chart`, `timechart`, `top`, `rare`.

**Uso:** dashboards, alertas, reportes.

#### 6.4 Generating

Generan resultados **sin** una búsqueda base tradicional (no dependen de “buscar eventos” de la forma clásica).

Ejemplos: `| makeresults`, `| inputlookup`, `| tstats`, `| mstats`.

**Uso:** búsquedas sintéticas, consultas sobre lookups, consultas sobre modelos de datos acelerados.

#### 6.5 Orchestrating

Controlan o coordinan la ejecución de otras búsquedas.

Ejemplos: `map`, `multisearch`.

**Uso:** casos avanzados. Requieren cuidado especial con el rendimiento (por ejemplo, `map` puede lanzar una subbúsqueda por cada resultado de entrada).

#### 6.6 Dataset processing

Operan sobre el dataset completo, normalmente porque necesitan contexto global o secuencial (no evento a evento de forma aislada).

Ejemplos: `sort`, `eventstats`, `streamstats`, `transaction`.

**Uso:** cuando la lógica necesita ver “todo el conjunto” o una secuencia ordenada de eventos.

***

### 7. Sintaxis SPL simple

#### Buscar por índice

```
index=main
```

#### Buscar por sourcetype

```
index=main sourcetype=access_combined
```

#### Buscar texto literal

```
index=main "error"
```

#### Buscar frase exacta

```
index=main "failed login"
```

#### Buscar campo igual a un valor

```
index=main status=500
```

#### Buscar campo diferente de un valor

```
index=main status!=200
```

#### Buscar múltiples condiciones

```
index=main status=500 method=POST
```

En SPL, el operador `AND` suele estar **implícito** entre términos consecutivos en la búsqueda base. La documentación del comando `search` indica que `AND` está implícito entre términos y expresiones; por ejemplo, `web error` equivale a `web AND error`.

#### OR

```
index=main status=401 OR status=403
```

#### NOT

```
index=main status=500 NOT uri="/health"
```

#### Paréntesis

```
index=main (status=401 OR status=403) method=POST
```

#### Wildcards

**Bien** (wildcard al final, más eficiente):

```
index=main user=adm*
```

**Evitar** (wildcard al inicio, más costoso):

```
index=main user=*adm*
```

***

### 8. Sintaxis SPL compleja

#### Condiciones con `where`

```
index=proxy
| where bytes_out > 1000000 AND like(url, "%download%")
```

#### Evaluaciones condicionales

```
index=windows
| eval severity=case(
    EventCode=4625, "Medium",
    EventCode=4720, "High",
    EventCode=4728, "High",
    true(), "Low"
)
```

#### Campos calculados

```
index=proxy
| eval mb_out=round(bytes_out/1024/1024, 2)
```

#### Extracción con regex

```
index=app
| rex field=_raw "user=(?<user>[^\s]+)"
```

#### Búsqueda con subsearch

```
index=proxy
[ search index=threatintel indicator_type=domain
  | fields domain
  | rename domain AS query
]
```

#### Join

```
index=auth
| stats latest(_time) AS last_login BY user
| join type=left user [
    search index=identity
    | fields user department manager
]
```

`join` debe usarse con cuidado: suele ser costoso y tiene límites en el número de resultados de la subbúsqueda. Mejor usar `lookup`, `stats`, `eventstats` o `append` cuando la lógica lo permita.

#### Correlación con `stats`

```
index=windows EventCode IN (4624,4625,4720,4728)
| stats
    count(eval(EventCode=4625)) AS failed_logons
    count(eval(EventCode=4624)) AS successful_logons
    count(eval(EventCode=4720)) AS created_users
    count(eval(EventCode=4728)) AS group_additions
  BY user src_ip
| where failed_logons > 20 AND successful_logons > 0
```

La documentación oficial muestra que se pueden usar expresiones `eval` dentro de funciones de `stats`; por ejemplo, `count(eval(method="GET"))` y `count(eval(method="POST"))` para contar distintos tipos de eventos dentro de una misma fila de agregación. Esto es extremadamente útil para construir “pivots” de varias métricas en una sola pasada, sin necesitar múltiples búsquedas.

***

### 9. Operadores en profundidad

#### 9.1 Operadores booleanos

Sirven para unir o excluir condiciones. La documentación oficial indica que los operadores lógicos soportados en expresiones booleanas son `AND`, `OR`, `NOT` y `XOR`, y que **deben escribirse en mayúsculas**.

**AND**

Exige que se cumplan dos condiciones.

```
index=web status=500 method=POST
```

Es equivalente a:

```
index=web status=500 AND method=POST
```

En la búsqueda base, `AND` está implícito entre términos.

```
index=auth action=failure user=admin
```

Significa: `action` es `failure` **Y** `user` es `admin`.

**OR**

Acepta una condición u otra.

```
index=web status=401 OR status=403
```

Mejor con paréntesis explícitos:

```
index=web (status=401 OR status=403)
```

Ejemplo de seguridad:

```
index=windows (EventCode=4625 OR EventCode=4771)
```

Busca eventos de login fallido o de Kerberos pre-auth failed.

**NOT**

Excluye una condición.

```
index=web status=500 NOT uri="/health"
```

Busca errores 500, excepto los de `/health`.

**Importante:** `NOT` afecta normalmente al término que viene justo después de él. Si hay ambigüedad con `OR`, usa paréntesis:

```
index=web status=500 NOT (uri="/health" OR uri="/status")
```

**XOR**

Significa “uno u otro, pero no ambos”.

```
| where condition_a XOR condition_b
```

No se usa demasiado en trabajo de SOC habitual; normalmente basta con `AND`, `OR` y `NOT`.

#### 9.2 Operadores de comparación

| Operador | Significado   | Ejemplo                       |
| -------- | ------------- | ----------------------------- |
| `=`      | Igual a       | `index=web status=200`        |
| `!=`     | Distinto de   | `index=web status!=200`       |
| `>`      | Mayor que     | `where bytes_out > 1000000`   |
| `<`      | Menor que     | `where duration < 5`          |
| `>=`     | Mayor o igual | `where count >= 10`           |
| `<=`     | Menor o igual | `where response_time <= 1000` |

**Uso correcto:** para filtros simples, colócalos en la búsqueda base (`index=proxy action=blocked`). Para condiciones calculadas (que dependen de una expresión), usa `where` (`| where bytes_out > avg_bytes * 3`).

#### 9.3 Operadores matemáticos

Se usan normalmente con `eval` o `where`.

* **Suma (`+`)**: `| eval total=bytes_in + bytes_out`
* **Resta ()**: `| eval diff=latest_time - earliest_time`
* **Multiplicación ()**: `| eval threshold=avg_count * 3`
* **División (`/`)**: `| eval mb=bytes/1024/1024`
* **Módulo (`%`)**: devuelve el resto de una división. `| eval remainder=count%2`. Sirve, por ejemplo, para saber si un número es par o impar.

#### 9.4 Operador de concatenación

En SPL, para concatenar strings se usa el punto `.`.

```
| eval full_user=domain."\\".user
```

```
| eval message="User ".user." logged in from ".src_ip
```

#### 9.5 Operador IN

Comprueba si un valor está dentro de una lista de valores.

En búsqueda base:

```
index=windows EventCode IN (4624,4625,4720,4728)
```

Con `where`:

```
| where status IN (401,403,500)
```

Con `eval`:

```
| eval interesting=if(status IN (401,403,500), "yes", "no")
```

#### 9.6 Wildcards

El `*` representa “cualquier cosa” en un valor.

```
index=auth user=admin*
```

Busca usuarios que empiezan por `admin`.

```
index=web uri="/api/*"
```

**Mala práctica** (wildcard al inicio, más costoso porque no puede aprovechar el índice de términos):

```
index=auth user=*admin*
```

Mejor:

```
index=auth user=admin*
```

O normalizar antes con una función:

```
| eval user_type=if(match(user,"^admin"),"admin","normal")
```

#### 9.7 Comillas

* **Sin comillas**: para valores simples sin espacios. `status=500`.
* **Con comillas dobles**: obligatorio si el valor tiene espacios o caracteres especiales. `user="john.doe"`.
* **Frase exacta**: `index=app "failed login"` busca la frase literal completa.

***

### 10. Precedencia de operadores booleanos

Este es uno de los puntos donde más errores se cometen en SPL, porque **la precedencia cambia según el contexto**:

Según la documentación oficial, el orden de evaluación depende de si estás en `search`, `eval` o `where`:

* En la **búsqueda base (`search`)**: `OR` se evalúa **antes** que `AND`.
* En **`eval`** y **`where`**: `AND` se evalúa **antes** que `OR` (como en la mayoría de lenguajes de programación).

Por eso, **usa paréntesis siempre que mezcles `AND` y `OR`** en la misma expresión, sin importar en qué comando estés. Es la única forma de garantizar que la query hace lo que tú crees que hace, y además hace la query mucho más legible para cualquier otro analista.

#### Ejemplo ambiguo

```
index=web status=401 OR status=403 method=POST
```

Esto puede no significar lo que esperas, ya que la precedencia entre `OR` y el `AND` implícito en la búsqueda base puede agrupar los términos de forma distinta a la intuitiva.

#### Versión correcta

```
index=web (status=401 OR status=403) method=POST
```

Esto significa sin ambigüedad: *status 401 o 403, y además method POST*.

***

### 11. Comandos SPL principales (catálogo detallado)

A continuación, el catálogo completo y detallado de comandos, con sintaxis, ejemplos y notas de uso.

#### 11.1 `search`

Recupera eventos o filtra resultados previos. Está implícito al inicio de toda búsqueda y puede usar keywords, frases entre comillas, wildcards y expresiones `campo=valor`.

```
search <logical-expression>
```

Ejemplos:

```
index=main error
index=main status=500
index=main (status=401 OR status=403) method=POST
```

#### 11.2 `fields`

Incluye o excluye campos del conjunto de resultados (afecta a qué campos siguen disponibles en la tubería, no al formato de salida visual).

Incluir:

```
| fields _time user src_ip action
```

Excluir (con `-`):

```
| fields - _raw _bkt _cd
```

Uso: reducir memoria, mejorar legibilidad, preparar resultados antes de un dashboard.

#### 11.3 `table`

Muestra campos en forma tabular como **salida final** de presentación.

```
| table _time user src_ip action
```

**Diferencia clave con `fields`:** `fields` controla qué campos continúan disponibles en el pipeline (para procesamiento posterior); `table` formatea la salida final visual. Por eso `table` normalmente va al **final** de la query, mientras que `fields` puede ir en medio para optimizar.

#### 11.4 `rename`

Renombra campos.

```
| rename src_ip AS source_ip
```

Múltiples renombrados en la misma línea:

```
| rename src_ip AS source_ip dest_ip AS destination_ip
```

#### 11.5 `eval`

Calcula expresiones y guarda el resultado en un campo. Si el campo no existe, lo crea; si existe, lo **sobrescribe**. La documentación oficial indica que `eval` evalúa expresiones matemáticas, de string y booleanas, y permite encadenar múltiples expresiones separadas por comas dentro del mismo comando.

Sintaxis:

```
| eval field=expression
```

Ejemplos:

```
| eval status_group=if(status>=500, "server_error", "other")
| eval mb=round(bytes/1024/1024, 2)
| eval user=lower(user)
```

#### 11.6 `where`

Filtra usando expresiones evaluadas (más potente que un filtro simple de búsqueda base, pero más costoso).

```
| where count > 10
| where like(url, "%admin%")
| where cidrmatch("10.0.0.0/8", src_ip)
```

**Diferencia importante:**

```
status=500
```

es un filtro de **búsqueda base** (eficiente, indexable).

```
| where status=500
```

es un filtro **posterior** en la tubería y normalmente menos eficiente si la condición se puede expresar antes.

#### 11.7 `stats`

Calcula estadísticas agregadas, colapsando los eventos en filas de resumen.

```
| stats count BY user
| stats count dc(src_ip) AS unique_sources BY user
| stats earliest(_time) AS first_seen latest(_time) AS last_seen BY user
```

La documentación oficial indica que `stats`, `streamstats` y `eventstats` calculan estadísticas sobre resultados o eventos, con diferencias en cómo devuelven los valores (ver sección de comparativas).

#### 11.8 `eventstats`

Agrega estadísticas a **cada evento** sin colapsar el conjunto de resultados (a diferencia de `stats`).

```
index=proxy
| eventstats avg(bytes_out) AS avg_bytes BY user
| where bytes_out > avg_bytes * 3
```

Uso: comparar un evento contra una línea base (baseline), manteniendo los eventos originales disponibles, útil en detecciones de anomalías.

#### 11.9 `streamstats`

Calcula estadísticas de forma **secuencial**, evento a evento, en el orden en que aparecen.

```
index=auth
| sort 0 user _time
| streamstats count AS attempt_number BY user
```

Uso: numerar intentos sucesivos, calcular contadores progresivos, ventanas móviles por entidad, detección de patrones temporales (por ejemplo, “el 5º intento fallido en menos de 1 minuto”).

#### 11.10 `chart`

Crea una tabla preparada para gráficos. Es un comando **transformador** que devuelve resultados en formato tabla para visualizaciones como columnas, líneas, áreas o gráficos circulares (pie chart); requiere una función estadística.

```
index=web
| chart count BY status
index=web
| chart count OVER host BY status
```

#### 11.11 `timechart`

Agrupa resultados por intervalos de tiempo.

```
index=web status=500
| timechart span=1h count
index=auth EventCode=4625
| timechart span=15m count BY user limit=10
```

Uso: tendencias, dashboards, volumen a lo largo del tiempo, detección de picos.

#### 11.12 `top`

Muestra los valores más frecuentes de un campo.

```
index=proxy
| top user
index=proxy
| top limit=20 dest_domain BY user
```

#### 11.13 `rare`

Muestra los valores **menos** frecuentes (lo opuesto a `top`).

```
index=dns
| rare query
```

Muy útil en threat hunting para encontrar: dominios raros, procesos raros, user-agents raros (valores “de cola larga” que destacan por ser poco comunes).

#### 11.14 `sort`

Ordena resultados.

```
| sort - count
| sort 0 - _time
```

Notas importantes:

* `sort` puede ser costoso porque requiere centralizar todos los resultados.
* `sort 0` significa “sin límite de resultados a ordenar” (por defecto `sort` limita a los primeros 10.000).
* Evita ordenar millones de eventos si no es estrictamente necesario; agrega primero cuando sea posible.

#### 11.15 `dedup`

Elimina eventos duplicados según uno o más campos.

```
| dedup user
| dedup user sortby -_time
```

Uso: quedarte con el último evento por entidad, evitar duplicados en resultados.

**Alternativa más controlada y a menudo más clara:**

```
| stats latest(_time) AS last_seen BY user
```

#### 11.16 `rex`

Extrae campos usando expresiones regulares.

```
| rex field=_raw "user=(?<user>[^\s]+)"
```

Modo `sed` (para sustituciones tipo `sed`):

```
| rex mode=sed field=user "s/@domain.com//g"
```

Uso: extraer campos que no se parsean automáticamente, normalizar strings, construir detecciones sobre patrones en texto crudo.

#### 11.17 `regex`

Filtra eventos usando una expresión regular como condición (no extrae, solo filtra).

```
| regex user="^adm"
```

**Diferencia clave:** `rex` **extrae** un nuevo campo; `regex` **filtra** eventos existentes.

#### 11.18 `spath`

Parsea datos estructurados JSON/XML.

```
| spath
| spath input=_raw path=user.name output=user
```

Uso: logs en formato JSON, logs de nube (cloud), logs de API, telemetría de EDR.

#### 11.19 `lookup`

Enriquece eventos con datos de un CSV o de un KV Store.

```
| lookup users.csv user OUTPUT department manager
| lookup threatintel.csv dest_domain AS domain OUTPUT category confidence
```

Uso: inventario de activos, threat intelligence, datos de identidad, criticidad de activos, listas de usuarios VIP.

#### 11.20 `inputlookup`

Lee un lookup como si fuera un dataset de búsqueda (sin necesidad de un índice).

```
| inputlookup users.csv
| inputlookup threatintel.csv
| search confidence=high
```

#### 11.21 `outputlookup`

Guarda los resultados actuales de la búsqueda en un archivo lookup.

```
| outputlookup suspicious_users.csv
```

**Cuidado:** puede **sobrescribir** datos existentes. Controla los permisos y evita usarlo accidentalmente en búsquedas exploratorias (podrías destruir un lookup en producción sin querer).

#### 11.22 `join`

Une resultados de dos búsquedas por un campo común.

```
index=auth
| stats latest(_time) AS last_login BY user
| join user [
    search index=identity
    | fields user department
]
```

Problemas conocidos: es costoso, tiene un límite de resultados en la subbúsqueda, y puede “perder” resultados que no encuentran coincidencia (dependiendo del tipo de join). **Mejor usar `lookup` o `stats` cuando sea posible.**

#### 11.23 `append`

Añade los resultados de otra búsqueda al final del conjunto actual (concatenación de filas).

```
index=auth action=failure
| stats count AS failures BY user
| append [
    search index=auth action=success
    | stats count AS successes BY user
]
```

#### 11.24 `appendcols`

Añade columnas de otra búsqueda **por posición de fila** (no por clave de coincidencia).

```
search index=a
| stats count AS count_a
| appendcols [
    search index=b
    | stats count AS count_b
]
```

**Cuidado:** une por número de fila, no por una clave común; si el orden de las filas no coincide conceptualmente, el resultado será incorrecto.

#### 11.25 `transaction`

Agrupa eventos relacionados en una única “transacción” (por ejemplo, todos los eventos de una sesión).

```
index=web
| transaction session_id startswith="login" endswith="logout"
```

Uso: sesiones, secuencias con inicio/fin, flujos transaccionales.

**Cuidado:** es muy costoso computacionalmente y consume mucha memoria. Evitar en grandes volúmenes de datos. Alternativas más eficientes: `stats`, `streamstats`.

#### 11.26 `bin` / `bucket`

Agrupa valores continuos en intervalos discretos (buckets).

```
| bin _time span=1h
| stats count BY _time

| bin bytes span=1000000
| stats count BY bytes
```

#### 11.27 `fillnull`

Rellena valores nulos/vacíos con un valor por defecto.

```
| fillnull value="unknown" user department
```

#### 11.28 `filldown`

Rellena valores nulos con el último valor no nulo visto anteriormente (hacia abajo en la tabla).

```
| filldown user
```

#### 11.29 `coalesce` (función, no comando)

Devuelve el primer valor no nulo entre varios candidatos. Se aclara aquí porque suele confundirse con un comando, pero es una **función** usada dentro de `eval`.

```
| eval src=coalesce(src_ip, source_ip, client_ip)
```

#### 11.30 `makemv`

Convierte un campo de texto (string) en un campo multivalor, dividiéndolo por un delimitador.

```
| makemv delim="," users
```

#### 11.31 `mvexpand`

Expande un campo multivalor en **varias filas** (una fila por cada valor).

```
| mvexpand users
```

**Cuidado:** puede multiplicar masivamente el número de eventos resultantes; úsalo después de filtrar todo lo posible, nunca al principio de una query pesada.

#### 11.32 `nomv`

Convierte un campo multivalor en un único string.

```
| nomv users
```

#### 11.33 `head`

Devuelve los primeros N resultados.

```
| head 10
```

#### 11.34 `tail`

Devuelve los últimos N resultados.

```
| tail 10
```

#### 11.35 `reverse`

Invierte el orden actual de los resultados.

```
| reverse
```

#### 11.36 `replace`

Reemplaza valores concretos por otros.

```
| replace "N/A" WITH "unknown" IN user
```

#### 11.37 `convert`

Convierte tipos de datos o formatos (por ejemplo, epoch a fecha legible).

```
| convert ctime(_time) AS readable_time
```

#### 11.38 `fieldformat`

Formatea la **visualización** de un campo sin cambiar su valor interno real. Esto es importante: el dato subyacente sigue siendo el mismo, solo cambia cómo se muestra.

```
| fieldformat readable_time=strftime(_time, "%Y-%m-%d %H:%M:%S")
```

#### 11.39 `iplocation`

Geolocaliza direcciones IP.

```
| iplocation src_ip
```

Campos típicos generados: `Country`, `Region`, `City`, `lat`, `lon`.

#### 11.40 `geostats`

Agrega datos geográficos (pensado para usarse junto a mapas en dashboards).

```
| geostats count BY Country
```

#### 11.41 `collect`

Escribe los resultados actuales en un índice de resumen (summary index).

```
| collect index=summary marker="auth_baseline"
```

Uso: summary indexing, reportes pesados que se calculan una vez y se reutilizan, detecciones aceleradas.

#### 11.42 `metadata`

Obtiene metadatos sobre los índices (no eventos, sino información estructural).

```
| metadata type=hosts index=main
```

Uso: ver qué hosts, sources y sourcetypes existen; health checks del entorno Splunk.

#### 11.43 `dbxquery`

Si está instalado el add-on Splunk DB Connect, permite ejecutar consultas SQL contra bases de datos externas.

```
| dbxquery query="SELECT * FROM users" connection="my_connection"
```

#### 11.44 `mstats`

Para consultar datos de métricas (metric indexes).

```
| mstats avg(cpu.usage) WHERE index=metrics BY host span=5m
```

#### 11.45 `tstats`

Estadísticas sobre campos indexados o modelos de datos acelerados. Mucho más rápido que `stats` cuando aplica, porque no necesita leer los eventos crudos, solo los metadatos indexados.

```
| tstats count WHERE index=firewall BY host sourcetype
```

Con data model:

```
| tstats count FROM datamodel=Authentication.Authentication
  WHERE Authentication.action=failure
  BY Authentication.user Authentication.src
```

#### 11.46 `from`

Búsqueda sobre datasets definidos (por ejemplo, data models).

```
| from datamodel:"Authentication"."Authentication"
| stats count BY Authentication.user
```

#### 11.47 `savedsearch`

Ejecuta una búsqueda previamente guardada por su nombre.

```
| savedsearch "Nombre de la búsqueda"
```

#### 11.48 `makeresults`

Crea eventos sintéticos (no provienen de ningún índice real).

```
| makeresults
| eval user="test", action="login"
```

Uso: pruebas, ejemplos didácticos, validación de lógica de `eval`/`case`/etc. sin depender de datos reales.

#### 11.49 `map`

Ejecuta una subbúsqueda por **cada resultado** de la búsqueda de entrada.

```
| inputlookup users.csv
| map search="search index=auth user=$user$ | stats count BY user"
```

**Cuidado:** muy costoso, puede lanzar un número muy alto de búsquedas independientes. Evitar salvo que esté claramente justificado.

#### 11.50 `format`

Convierte un conjunto de resultados en una expresión de búsqueda (útil para construir subsearches manualmente o depurar cómo Splunk convertiría un subsearch).

```
| inputlookup suspicious_ips.csv
| fields src_ip
| format
```

***

### 12. Funciones de evaluación (eval functions)

Las funciones de evaluación se usan sobre todo con `eval`, `where` y `fieldformat`. La documentación oficial indica que las *evaluation functions* se usan para evaluar expresiones basadas en eventos y devolver un resultado, y que se pueden usar con `eval`, `fieldformat`, `where` y dentro de expresiones `eval` junto a otros comandos.

#### 12.1 Funciones condicionales

**`if(condición, valor_si_true, valor_si_false)`**

Toma una decisión binaria simple.

```
| eval result=if(status=200, "ok", "error")
```

Ejemplo SOC:

```
| eval severity=if(failed_logons>=10,"high","medium")
```

**`case(condición1, valor1, condición2, valor2, ...)`**

Evalúa varias condiciones en orden y devuelve el valor asociado a la **primera** que sea verdadera. La documentación oficial explica que `case()` acepta pares de condición-valor y devuelve el valor de la primera condición que evalúa como verdadera.

```
| eval severity=case(
    count > 100, "Critical",
    count > 50, "High",
    count > 10, "Medium",
    true(), "Low"
)
```

**`true()`**

Se usa como condición “por defecto” (catch-all) dentro de `case()`, ya que siempre evalúa a verdadero.

```
| eval category=case(status>=500, "server", status>=400, "client", true(), "other")
```

**`false()`**

Representa una condición que siempre es falsa. No se usa tanto de forma directa, pero puede ser útil dentro de expresiones lógicas complejas o para pruebas.

```
| eval flag=false()
```

**`validate(condición, valor, ...)`**

Evalúa condiciones y devuelve un mensaje/valor asociado a la **primera condición que es falsa** (funciona de forma inversa a `case`, pensado para validaciones).

```
| eval validation=validate(isnotnull(user), "missing_user")
```

Uso práctico para encontrar registros con datos faltantes:

```
index=auth
| eval validation=validate(isnotnull(user),"missing_user")
| where isnotnull(validation)
```

#### 12.2 Funciones de comparación y nulos

**`isnull(campo)`**

Comprueba si un campo está vacío o no existe.

```
| where isnull(user)
```

**`isnotnull(campo)`**

Comprueba si un campo existe y tiene valor.

```
| where isnotnull(src_ip)
```

**`null()`**

Genera explícitamente un valor nulo. Útil para “limpiar” valores que representan ausencia de dato (como `-`).

```
| eval user=if(user="-", null(), user)
```

**`nullif(valor1, valor2)`**

Devuelve `null` si ambos valores son iguales; en caso contrario devuelve el primer valor.

```
| eval clean_user=nullif(user,"-")
```

Si `user` es `-`, `clean_user` será `null`.

**`coalesce(valor1, valor2, valor3, ...)`**

Devuelve el primer valor no nulo de la lista. Es una de las funciones más usadas para **normalizar campos** que pueden venir con distintos nombres según la fuente de datos.

```
| eval src_ip=coalesce(src_ip, source_ip, clientip)
```

Ejemplo SOC:

```
| eval user=coalesce(user, username, account_name, src_user)
```

#### 12.3 Funciones de string (texto)

**`lower(campo)`**

Convierte texto a minúsculas. Muy usada para normalizar (evitar que `ADMIN`, `Admin` y `admin` se traten como valores distintos).

```
| eval user=lower(user)
```

**`upper(campo)`**

Convierte texto a mayúsculas.

```
| eval country=upper(country)
```

**`len(campo)`**

Devuelve la longitud (número de caracteres) del texto.

```
| eval user_length=len(user)
```

```
| where len(password)>20
```

**`substr(campo, inicio, longitud)`**

Extrae una subcadena. **Importante:** en SPL, la posición inicial suele empezar en 1 (no en 0, a diferencia de muchos lenguajes de programación).

```
| eval prefix=substr(user,1,3)
```

```
| eval domain=substr(email,1,5)
```

**`replace(campo, regex, reemplazo)`**

Reemplaza texto usando una expresión regular.

```
| eval clean_user=replace(user, "@domain.com", "")
```

```
| eval normalized_path=replace(file_path,"\\\\","/")
```

**`trim(campo)`**

Elimina espacios en blanco al principio y al final del texto.

```
| eval user=trim(user)
```

**`ltrim(campo)`**

Elimina espacios en blanco solo del lado izquierdo.

```
| eval user=ltrim(user)
```

**`rtrim(campo)`**

Elimina espacios en blanco solo del lado derecho.

```
| eval user=rtrim(user)
```

**`split(campo, delimitador)`**

Divide un string en un campo **multivalor** según un delimitador.

```
| eval parts=split(email, "@")
```

```
| eval domain=mvindex(split(email,"@"),1)
```

**`mvjoin(multivalue, delimitador)`**

Convierte un campo multivalor en un único string, uniendo los valores con un delimitador.

```
| eval users_string=mvjoin(users, ",")
```

**`match(campo, regex)`**

Devuelve verdadero/falso según si el campo cumple una expresión regular completa.

```
| where match(user, "^adm")
```

Ejemplo SOC:

```
| where match(lower(command_line), "powershell.*encodedcommand")
```

**`like(campo, patrón)`**

Busca coincidencias usando patrones simples con `%` como comodín (similar a `LIKE` en SQL).

```
| where like(url, "%admin%")
```

```
| where like(command_line,"% -enc %")
```

**Diferencia clave:** `match()` usa expresiones regulares completas (más potente, más complejo); `like()` usa un patrón simple con `%` (más sencillo de leer, menos flexible).

#### 12.4 Funciones matemáticas

**`round(número, decimales)`**

Redondea un número al número de decimales indicado.

```
| eval mb=round(bytes/1024/1024, 2)
```

**`floor(número)`**

Redondea hacia abajo (al entero inferior más cercano).

```
| eval value=floor(score)
```

Ejemplo: `floor(4.9) = 4`.

**`ceil(número)`**

Redondea hacia arriba (al entero superior más cercano).

```
| eval value=ceil(score)
```

Ejemplo: `ceil(4.1) = 5`.

**`abs(número)`**

Devuelve el valor absoluto.

```
| eval diff=abs(current-baseline)
```

**`sqrt(número)`**

Raíz cuadrada.

```
| eval root=sqrt(value)
```

**`pow(base, exponente)`**

Potencia.

```
| eval squared=pow(value,2)
```

**`log(valor)`**

Logaritmo (típicamente en base 10, según implementación).

```
| eval log_value=log(value)
```

**`ln(valor)`**

Logaritmo natural (base *e*).

```
| eval natural_log=ln(value)
```

**`exp(valor)`**

Función exponencial ($e^{valor}$).

```
| eval exponential=exp(value)
```

**`pi()`**

Devuelve el valor de $\pi$.

```
| eval circle_area=pi()*pow(radius,2)
```

**`random()`**

Genera un número entero aleatorio.

```
| eval random_id=random()
```

Uso típico junto con `makeresults` para pruebas:

```
| makeresults
| eval sample=random()
```

**`sigfig(valor)`**

Ajusta un número a sus cifras significativas.

```
| eval clean_value=sigfig(value)
```

**`tonumber(valor)`**

Convierte un valor de texto a número. Fundamental cuando un campo numérico se extrajo como string y necesitas compararlo con operadores como `>`.

```
| eval status_num=tonumber(status)
```

**`tostring(valor)`**

Convierte un valor a texto. También admite un segundo parámetro para dar formato (por ejemplo, formato de duración).

```
| eval status_str=tostring(status)
| eval duration_text=tostring(duration,"duration")
```

#### 12.5 Funciones de fecha y tiempo

**`_time`**

No es una función, es el **campo temporal principal** de cada evento (en formato epoch internamente).

```
| table _time user action
```

**`now()`**

Devuelve el momento actual en formato epoch (segundos desde 1970-01-01 UTC).

```
| eval age_seconds=now()-_time
```

```
| where now()-_time < 3600
```

(eventos de la última hora).

**`time()`**

Devuelve el momento de ejecución de la evaluación (similar a `now()`, usado en contextos de procesamiento).

```
| eval processing_time=time()
```

**`relative_time(tiempo, especificador)`**

Calcula un tiempo relativo a partir de un instante base, usando modificadores de tiempo de Splunk (por ejemplo, `@h` para inicio de hora, `-1d` para “hace un día”).

```
| eval start_hour=relative_time(now(), "@h")
```

Otros ejemplos:

```
relative_time(now(), "-1h")
relative_time(now(), "-1d@d")
relative_time(now(), "@d")
```

**`strftime(epoch, formato)`**

Convierte un valor epoch a una cadena de texto legible según un formato especificado (similar a `strftime` en C/Python).

```
| eval readable_time=strftime(_time, "%Y-%m-%d %H:%M:%S")
```

```
| eval day=strftime(_time,"%Y-%m-%d")
```

**`strptime(string, formato)`**

Convierte una cadena de texto a epoch, interpretándola según un formato dado (operación inversa a `strftime`).

```
| eval parsed_time=strptime(timestamp, "%Y-%m-%d %H:%M:%S")
```

```
| eval event_epoch=strptime(event_date,"%d/%m/%Y %H:%M:%S")
```

#### 12.6 Funciones de IP y red

**`cidrmatch(cidr, ip)`**

Comprueba si una IP pertenece a un rango de red expresado en notación CIDR.

```
| where cidrmatch("10.0.0.0/8", src_ip)
```

Ejemplo SOC (detectar tráfico saliente hacia IPs que **no** son internas):

```
index=proxy
| where NOT cidrmatch("10.0.0.0/8",dest_ip)
```

**`ipmask(máscara, ip)`**

Aplica una máscara de subred a una IP, útil para agrupar por subred.

```
| eval subnet=ipmask("255.255.255.0", src_ip)
```

#### 12.7 Funciones criptográficas / hashing

La documentación oficial indica que las funciones criptográficas se usan para calcular hashes seguros sobre campos de tipo string o strings literales. Dependen de la versión de Splunk, pero suelen incluir:

**`md5(texto)`**

```
| eval user_hash=md5(user)
```

**`sha1(texto)`**

```
| eval file_sha1=sha1(file_name)
```

**`sha256(texto)`**

```
| eval user_hash=sha256(user)
```

**`sha512(texto)`**

Disponible si la versión lo soporta.

```
| eval hash=sha512(value)
```

Uso: normalización de identificadores, pseudonimización de datos sensibles, comparación de hashes contra indicadores de compromiso (IOCs).

```
| eval normalized_hash=lower(sha256(file_path))
```

#### 12.8 Funciones de tipo e información

Comprueban qué tipo de dato tiene un valor.

**`isnum(valor)` - comprueba si es numérico**

```
| where isnum(bytes)
```

**`isstr(valor)` - comprueba si es string**

```
| where isstr(user)
```

**`isint(valor)` - comprueba si es entero**

```
| where isint(status)
```

**`isbool(valor)` - comprueba si es booleano**

```
| where isbool(flag)
```

**`typeof(valor)` - devuelve el tipo de dato como texto**

```
| eval field_type=typeof(bytes)
```

Uso práctico:

```
index=proxy
| eval bytes_type=typeof(bytes)
| stats count BY bytes_type
```

***

### 13. Funciones estadísticas

Se usan con `stats`, `chart`, `timechart`, `eventstats`, `streamstats` y comandos relacionados (`geostats`, `sistats`, `sichart`, `sitimechart`). La documentación oficial (*Statistical and charting functions*) indica que la mayoría espera valores numéricos, aunque algunas procesan también valores de tipo string, como `count`, `distinct_count`, `earliest`, `first`, `latest`, `last`, `list`, `max`, `min`, `mode` y `values`.

#### `count`

Cuenta eventos.

```
| stats count
| stats count BY user
| stats count AS total_events BY user
```

#### `count(campo)`

Cuenta eventos donde ese campo existe (no es nulo).

```
| stats count(user) AS events_with_user
```

#### `dc(campo)` / `distinct_count(campo)`

Cuenta valores **únicos** de un campo.

```
| stats dc(src_ip) AS unique_sources BY user
```

Uso SOC:

```
index=auth action=failure
| stats count AS failures dc(src_ip) AS unique_sources BY user
```

#### `estdc(campo)`

Cuenta valores únicos de forma **estimada** (más rápido, aproximado). Útil en grandes volúmenes de datos donde un `dc()` exacto sería demasiado costoso.

```
| stats estdc(user) AS estimated_users
```

#### `estdc_error(campo)`

Devuelve el margen de error estimado de `estdc()`.

```
| stats estdc(user) AS estimated_users estdc_error(user) AS error
```

#### `sum(campo)`

Suma valores numéricos.

```
| stats sum(bytes) AS total_bytes BY user
```

#### `avg(campo)`

Calcula la media.

```
| stats avg(duration) AS avg_duration BY app
```

#### `min(campo)`

Valor mínimo.

```
| stats min(_time) AS first_time BY user
```

#### `max(campo)`

Valor máximo.

```
| stats max(_time) AS last_time BY user
```

#### `range(campo)`

Diferencia entre el valor máximo y el mínimo. Útil, por ejemplo, para aproximar la duración de una sesión usando `_time`.

```
| stats range(_time) AS duration BY session_id
```

#### `median(campo)`

Mediana (valor central de la distribución).

```
| stats median(duration) BY app
```

#### `mode(campo)`

Valor más frecuente.

```
| stats mode(country) AS most_common_country BY user
```

#### `stdev(campo)`

Desviación estándar de **muestra**.

```
| stats avg(bytes) AS avg_bytes stdev(bytes) AS stdev_bytes BY user
```

#### `stdevp(campo)`

Desviación estándar **poblacional** (usa todo el conjunto, no una muestra).

```
| stats stdevp(bytes) AS stdevp_bytes BY user
```

#### `var(campo)`

Varianza de muestra.

```
| stats var(bytes) AS variance BY user
```

#### `varp(campo)`

Varianza poblacional.

```
| stats varp(bytes) AS variance_population BY user
```

#### `perc<N>(campo)`

Percentil aproximado (por ejemplo, `perc95` = percentil 95).

```
| stats perc95(response_time) AS p95 BY app
```

Otros ejemplos: `perc50`, `perc90`, `perc99`.

#### `exactperc<N>(campo)`

Percentil exacto (más preciso pero potencialmente más costoso computacionalmente que la versión aproximada).

```
| stats exactperc95(response_time) AS exact_p95 BY app
```

#### `upperperc<N>(campo)`

Percentil con estimación por el límite superior.

```
| stats upperperc95(response_time) AS upper_p95 BY app
```

#### `earliest(campo)`

Valor más antiguo **según el tiempo** (`_time`) del evento.

```
| stats earliest(src_ip) AS first_src BY user
```

#### `latest(campo)`

Valor más reciente según el tiempo del evento.

```
| stats latest(_time) AS last_seen BY user
```

#### `first(campo)`

Primer valor según el **orden actual** de los resultados en el pipeline (no necesariamente el más antiguo en el tiempo).

```
| stats first(status) AS first_status BY session_id
```

#### `last(campo)`

Último valor según el orden actual de los resultados.

```
| stats last(status) AS last_status BY session_id
```

**Diferencia clave:** `earliest`/`latest` dependen del **tiempo** (`_time`); `first`/`last` dependen del **orden actual** en que Splunk procesa los resultados (que por defecto suele ser del más reciente al más antiguo, pero puede cambiar si hay un `sort` previo).

#### `values(campo)`

Devuelve los valores **únicos** de un campo como una lista multivalor. La documentación oficial indica que `values()` devuelve una lista de valores distintos como campo multivalor, y que el **orden es lexicográfico** (alfabético/orden de texto), no el orden de aparición.

```
| stats values(src_ip) AS src_ips BY user
```

#### `list(campo)`

Devuelve una lista de valores **preservando el orden de los eventos** (incluye repetidos). La documentación oficial indica que `list(<valor>)` devuelve una entrada multivalor a partir de los valores de un campo, que el orden refleja el orden de los eventos, y que **si hay más de 100 valores, solo devuelve los primeros 100**.

```
| stats list(action) AS actions BY user
```

#### `sparkline(...)`

Crea una mini gráfica de tendencia embebida dentro de una fila de tabla (usado típicamente en dashboards).

```
| stats sparkline(count) AS trend count BY host
```

#### Funciones estadísticas dentro de `eval`

Algunas funciones estadísticas también se pueden usar directamente dentro de `eval`, operando sobre una lista fija de valores (no sobre un conjunto de eventos):

```
| eval average_score=avg(score1,score2,score3)
| eval max_value=max(bytes_in,bytes_out)
| eval min_value=min(bytes_in,bytes_out)
| eval total=sum(bytes_in,bytes_out)
```

Esto es distinto de usar `avg()`/`max()`/etc. dentro de `stats`: aquí se aplican sobre un conjunto explícito de argumentos dentro de un mismo evento, no sobre múltiples eventos agrupados.

***

### 14. Funciones multivalue

Un campo multivalor es un campo que contiene **varios valores dentro del mismo evento**. Ejemplo conceptual:

```
src_ips = 10.0.0.1, 10.0.0.2, 10.0.0.3
```

La documentación oficial indica que las funciones multivalue se usan sobre campos multivalor, o para generar campos multivalor a partir de otros datos.

#### `mvappend(valor1, valor2, ...)`

Une varios valores en un único campo multivalor.

```
| eval all_ips=mvappend(src_ip,dest_ip)
| eval indicators=mvappend(domain, url, file_hash)
```

#### `mvcount(campo)`

Cuenta cuántos valores tiene un campo multivalor.

```
| eval number_of_ips=mvcount(src_ips)
| where mvcount(src_ips)>5
```

#### `mvdedup(campo)`

Elimina valores duplicados dentro de un multivalor.

```
| eval unique_ips=mvdedup(src_ips)
```

#### `mvfilter(condición)`

Filtra los valores de un campo multivalor según una condición, devolviendo solo los que la cumplen.

```
| eval admin_users=mvfilter(match(users,"^admin"))
| eval private_ips=mvfilter(cidrmatch("10.0.0.0/8",src_ips))
```

#### `mvfind(campo, regex)`

Devuelve la posición (índice) del primer valor que cumple una expresión regular.

```
| eval position=mvfind(users,"admin")
```

#### `mvindex(campo, posición)`

Devuelve el valor en una posición concreta de un campo multivalor (índice basado en 0).

```
| eval first_ip=mvindex(src_ips,0)
| eval domain=mvindex(split(email,"@"),1)
```

#### `mvjoin(campo, delimitador)`

Convierte un campo multivalor en un único string, uniendo con un delimitador.

```
| eval ips_text=mvjoin(src_ips,", ")
```

#### `mvmap(campo, expresión)`

Aplica una expresión a **cada valor** de un campo multivalor, devolviendo un nuevo multivalor transformado.

```
| eval lower_users=mvmap(users, lower(users))
```

#### `mvrange(inicio, fin, paso)`

Genera una lista de números como campo multivalor.

```
| eval numbers=mvrange(1,10,1)
```

#### `mvsort(campo)`

Ordena los valores dentro de un campo multivalor.

```
| eval sorted_ips=mvsort(src_ips)
```

#### `mvzip(campo1, campo2, separador)`

Combina dos campos multivalor **por posición**, generando pares.

```
| eval pairs=mvzip(users,src_ips,"=")
```

Resultado conceptual:

```
user1=10.0.0.1
user2=10.0.0.2
```

#### `commands(texto)`

Devuelve los comandos SPL usados dentro de una cadena de búsqueda (función de introspección). La documentación oficial indica que normalmente **no se recomienda** su uso salvo para análisis del `audit.log`.

```
| eval used_commands=commands("search foo | stats count | sort count")
```

***

### 15. Funciones y comandos para JSON/XML

#### `spath` (comando)

Extrae campos de datos estructurados en formato JSON o XML.

```
| spath
```

Extraer un campo concreto por su ruta (path):

```
| spath input=_raw path=user.name output=user
```

Ejemplo con logs cloud (formato JSON):

```
index=cloud sourcetype=json
| spath
| stats count BY userIdentity.userName eventName
```

#### `json_object(...)` (función)

Si está disponible en la versión de Splunk, crea un objeto JSON a partir de pares clave-valor.

```
| eval json=json_object("user",user,"src_ip",src_ip)
```

#### `json_array(...)` (función)

Crea un array JSON a partir de una lista de valores.

```
| eval array=json_array(user,src_ip,action)
```

> **Nota:** algunas funciones JSON dependen de la versión concreta de Splunk instalada; conviene verificar disponibilidad en el entorno antes de depender de ellas en detecciones productivas.

#### Diferencia entre `eval`, `where` y `fieldformat`

* **`eval`**: crea o modifica campos (cambia el dato real). `| eval user=lower(user)`
* **`where`**: filtra usando expresiones evaluadas. `| where count > 10`
* **`fieldformat`**: cambia **cómo se muestra** un campo, sin alterar su valor interno real.

```
index=auth
| stats latest(_time) AS last_seen BY user
| fieldformat last_seen=strftime(last_seen,"%Y-%m-%d %H:%M:%S")
```

***

### 16. Campos importantes en Splunk

#### 16.1 Campos internos

Estos campos son generados automáticamente por Splunk y suelen empezar con `_`:

| Campo         | Descripción                                                                                  |
| ------------- | -------------------------------------------------------------------------------------------- |
| `_time`       | Tiempo del evento.                                                                           |
| `_raw`        | El evento original, sin parsear.                                                             |
| `_indextime`  | Momento en que el evento fue indexado (puede diferir de `_time` por la latencia de ingesta). |
| `_index`      | Índice donde reside el evento.                                                               |
| `_sourcetype` | Sourcetype interno.                                                                          |
| `_source`     | Fuente del evento.                                                                           |
| `_host`       | Host de origen del evento.                                                                   |

#### 16.2 Campos comunes

Campos frecuentes en eventos de seguridad y de red:

```
host, source, sourcetype, index, user, src, src_ip, dest, dest_ip,
action, status, signature, process, process_name, parent_process,
command_line, file_name, file_hash, url, domain, bytes_in, bytes_out
```

#### 16.3 Campos CIM (Common Information Model)

En Splunk Enterprise Security se recomienda **normalizar** los datos a los campos definidos por el CIM, para que las detecciones funcionen de forma consistente independientemente de la fuente original de los datos:

```
Authentication.user
Authentication.src
Authentication.action
Network_Traffic.src
Network_Traffic.dest
Endpoint.Processes.process_name
Endpoint.Processes.process
```

El CIM es, en esencia, un “diccionario común de campos” que permite que una única búsqueda o data model funcione sobre datos de fabricantes distintos (firewalls, EDR, proxies, etc.), siempre que cada fuente se haya mapeado (normalizado) a esos nombres de campo estándar.

***

### 17. Macros

Las macros permiten **reutilizar fragmentos de SPL**, evitando duplicar la misma lógica en múltiples búsquedas.

#### 17.1 Macros sin argumentos

Son las más simples: un nombre entre acentos graves (\`\`\`) que Splunk sustituye literalmente por el SPL definido en `macros.conf` (o en la UI, en *Settings > Advanced Search > Search Macros*).

```
`windows_security_index`
EventCode=4625
| stats count BY user
```

Macro esperada (definida previamente en la configuración de Splunk):

```
(index=windows sourcetype=WinEventLog:Security)
```

Otros ejemplos típicos de macros sin argumentos:

```
`proxy_index`          -> (index=proxy sourcetype=bluecoat)
`edr_index`            -> (index=edr sourcetype=process)
`cim_authentication`   -> (index=windows OR index=linux OR index=okta)
```

#### 17.2 Macros con argumentos

Las macros también pueden recibir **parámetros**, lo que permite parametrizar detecciones completas y reutilizarlas con distintos valores sin duplicar código SPL.

Definición conceptual (en `macros.conf`):

```
[detect_bruteforce(2)]
definition = search $index_field$ | stats count AS failures BY user | where failures >= $threshold$
args = index_field, threshold
```

Uso en una búsqueda:

```
`detect_bruteforce("index=windows EventCode=4625", 10)`
```

Otro ejemplo, una macro que reciba el nombre de usuario y un umbral configurable:

```
`detect_bruteforce(user, threshold)`
```

Ventajas de las macros con argumentos:

* Permiten crear una **única definición de lógica de detección** (por ejemplo, “contar fallos y comparar contra un umbral”) y reutilizarla para distintos data sources o distintos umbrales por severidad.
* Facilitan el mantenimiento: si la lógica de detección cambia (por ejemplo, se añade una condición de exclusión), solo se cambia la macro y todas las detecciones que la usan se actualizan automáticamente.

#### 17.3 Buenas prácticas con macros

* **No hardcodear índices** directamente en cada detección: usa macros como `windows_security_index` en vez de escribir `index=windows sourcetype=WinEventLog:Security` en cada búsqueda. Si el índice cambia de nombre (migración, reorganización), solo hay que tocar la macro.
* **Centralizar sourcetypes**: si una misma fuente de datos puede tener sourcetypes ligeramente distintos entre entornos (por ejemplo, `WinEventLog:Security` vs `wineventlog`), la macro puede unificar esa variación en un solo lugar.
* **Centralizar filtros comunes**: exclusiones de cuentas de servicio, exclusiones de hosts de laboratorio, listas de “IPs internas”, etc., son candidatas perfectas para macros, ya que suelen reutilizarse en decenas de detecciones distintas.
* **Nombrar de forma descriptiva**: `windows_security_index`, `exclude_service_accounts`, `detect_bruteforce(user,threshold)` son mejores nombres que `macro1` o `idx1`.
* **Documentar cada macro**: qué hace, qué argumentos espera y qué asume sobre los datos (por ejemplo, que el campo `user` ya está en minúsculas).

Ventajas generales de usar macros:

* **Reutilización**: una misma definición se usa en muchas detecciones.
* **Mantenimiento centralizado**: si cambia el índice o sourcetype de una fuente, se actualiza en un solo lugar (la macro) en vez de en decenas de búsquedas.
* **Detecciones más limpias y legibles**: se abstraen detalles técnicos repetitivos.
* **Adaptación por entorno**: la misma detección puede funcionar en distintos entornos (dev, prod) simplemente cambiando la definición de la macro, sin tocar la lógica de detección.

***

### 18. Lookups

Un lookup es una tabla de referencia externa (CSV o KV Store) que se usa para **enriquecer** eventos con información adicional que no está presente en el evento original.

#### 18.1 CSV lookup

```
| lookup asset_inventory.csv host OUTPUT business_unit criticality owner
```

#### 18.2 KV Store lookup

Usado para datos dinámicos que cambian con frecuencia (a diferencia de un CSV estático).

```
| lookup identity_kv user OUTPUT department title
```

#### 18.3 Lookup para threat intelligence

```
index=dns
| lookup threat_domains.csv query AS domain OUTPUT confidence threat_type
| where isnotnull(confidence)
```

#### 18.4 Automatic lookups

Un **automatic lookup** se configura en `props.conf`/`transforms.conf` (o desde la UI, en *Settings > Lookups > Lookup definitions/Automatic lookups*) para que Splunk aplique el enriquecimiento **automáticamente** a todos los eventos de un sourcetype determinado, sin necesidad de escribir `| lookup ...` en cada búsqueda.

Ejemplo conceptual de configuración (`props.conf`):

```
[WinEventLog:Security]
LOOKUP-identity = identity_lookup user OUTPUT department title privileged
```

Con esto, cualquier búsqueda sobre `WinEventLog:Security` tendrá automáticamente los campos `department`, `title` y `privileged` disponibles, sin necesidad de invocar el lookup manualmente. Es muy útil para enriquecimientos que **siempre** deben aplicarse (por ejemplo, criticidad de activos o identidad de usuario), evitando que un analista se olvide de añadir el `| lookup` en una detección nueva.

#### 18.5 KV Store en detalle

El **KV Store** (Key-Value Store) es una base de datos tipo documento integrada en Splunk, pensada para datos que cambian con frecuencia (a diferencia de un CSV estático que hay que resubir manualmente).

Características clave:

* Se puede leer y escribir desde SPL (`| lookup`, `| inputlookup`, `| outputlookup` funcionan igual que con CSV) y también desde REST API o desde apps.
* Permite actualizaciones incrementales (añadir/editar una fila) sin reescribir todo el archivo, algo que con CSV es más costoso.
* Se usa mucho para: listas dinámicas de activos gestionados por otro sistema (CMDB), listas de excepciones que un analista actualiza desde un dashboard, o estado de detecciones (por ejemplo, “IPs ya investigadas”).

```
| lookup identity_kv user OUTPUT department title
```

#### 18.6 Lookup definitions vs lookup tables

* **Lookup table file**: el archivo de datos en sí (el `.csv` o la colección KV Store).
* **Lookup definition**: la configuración en Splunk que indica *cómo* usar ese archivo (qué campo de coincidencia usar, si es CIDR, si es case-sensitive, etc.). Una misma tabla puede tener varias definiciones distintas si se necesita coincidencia por distintos campos.

Esto es importante porque, al escribir `| lookup nombre_definicion campo OUTPUT campo2`, en realidad estás invocando la **definición**, no directamente el archivo.

#### 18.7 Tipos de coincidencia (lookup matching)

Splunk permite configurar cómo se hace el *match* entre el evento y el lookup:

* **Exact match** (por defecto): el valor del campo debe coincidir exactamente con una fila del lookup.
* **Case sensitive / case insensitive**: por defecto los lookups son case-sensitive; se puede configurar para que ignoren mayúsculas/minúsculas (importante para campos como `user` o `domain` que pueden venir con distinta capitalización según la fuente).
* **CIDR match**: permite hacer coincidir una IP contra un rango CIDR almacenado en el lookup (por ejemplo, un lookup de rangos de red internos por sede).

```
[cidr_lookup]
filename = internal_ranges.csv
match_type = CIDR(network)
```

* **Wildcard match**: permite coincidencias con dentro de los valores del lookup (por ejemplo, para hacer match de dominios como `.malicious-domain.com`).

```
[wildcard_lookup]
filename = threat_domains.csv
match_type = WILDCARD(domain)
```

#### 18.8 Rendimiento de lookups

* Los lookups con **muchas filas** (alta cardinalidad, por ejemplo millones de IOCs) pueden ralentizar significativamente una búsqueda, especialmente si se usan como *automatic lookup* sobre un sourcetype de alto volumen.
* Preferir lookups **pequeños y bien indexados** (por el campo de coincidencia) frente a lookups masivos genéricos.
* Si el volumen de datos de referencia es muy grande, valorar mover esa lógica a un **data model** o a una fuente indexada en vez de a un lookup CSV.
* Evitar lookups con `match_type=WILDCARD` sobre archivos grandes, ya que cada wildcard implica una comparación más costosa que un match exacto.
* Aplicar `| lookup` **después** de haber reducido el volumen de eventos con filtros previos, nunca como primer paso de la búsqueda.

***

### 19. Subsearches

Una subsearch (subbúsqueda) se ejecuta entre corchetes `[ ... ]` y su resultado se utiliza como entrada de la búsqueda principal (normalmente para generar una lista de valores a filtrar).

```
index=proxy [
  search index=threatintel type=domain confidence=high
  | fields domain
  | rename domain AS query
]
```

Problemas frecuentes con las subsearches:

* **Límite de resultados**: por defecto Splunk limita cuántos resultados puede devolver una subsearch (por ejemplo, unos pocos miles).
* **Límite de tiempo**: las subsearches tienen un tiempo máximo de ejecución configurado.
* **Pueden ser costosas**: se ejecutan antes que la búsqueda principal y pueden ralentizar considerablemente el conjunto completo.
* **A veces es mejor usar `lookup`**: si los datos de referencia son relativamente estáticos, un lookup en CSV suele ser más eficiente y predecible que una subsearch dinámica.

***

### 20. Detecciones de seguridad con SPL

#### 20.1 Brute force básico

```
index=windows sourcetype=WinEventLog:Security EventCode=4625 earliest=-15m@m latest=-5m@m
| stats count AS failed_logons dc(src_ip) AS unique_sources values(src_ip) AS src_ips BY user
| where failed_logons >= 10
| table user failed_logons unique_sources src_ips
```

**Qué hace:** cuenta los intentos de login fallido (`EventCode=4625`) por usuario en una ventana de 10 minutos (con offset de 5 minutos por latencia de ingesta), y marca como sospechosos a los usuarios con 10 o más fallos.

#### 20.2 Brute force seguido de login exitoso

```
index=windows sourcetype=WinEventLog:Security EventCode IN (4624,4625) earliest=-30m@m latest=-5m@m
| eval action=case(EventCode=4625,"failure",EventCode=4624,"success")
| stats
    count(eval(action="failure")) AS failures
    count(eval(action="success")) AS successes
    earliest(_time) AS first_seen
    latest(_time) AS last_seen
    values(src_ip) AS src_ips
  BY user
| where failures >= 10 AND successes >= 1
```

**Qué hace:** detecta usuarios con muchos fallos de login **y** al menos un login exitoso posterior, lo cual es un indicador clásico de un ataque de fuerza bruta que finalmente tuvo éxito.

#### 20.3 PowerShell sospechoso

```
index=windows sourcetype=WinEventLog:Security OR sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
("powershell" OR "pwsh")
| eval command=coalesce(CommandLine, Process_Command_Line, command_line, _raw)
| where match(lower(command), "(encodedcommand|downloadstring|iex|invoke-expression|bypass|hidden)")
| stats count values(command) AS commands values(host) AS hosts BY user
```

**Qué hace:** busca ejecuciones de PowerShell con parámetros o técnicas típicas de ataques (comandos codificados en base64, descarga remota de scripts, bypass de políticas de ejecución, ventanas ocultas).

#### 20.4 Ejecución desde rutas sospechosas

```
index=edr sourcetype=process
| eval process_path=lower(coalesce(process_path, Image, file_path))
| where match(process_path, "\\\\appdata\\\\|\\\\temp\\\\|\\\\programdata\\\\")
| stats count values(process_path) AS process_paths values(parent_process) AS parents BY host user process_name
```

**Qué hace:** identifica procesos que se ejecutan desde rutas típicamente usadas por malware (carpetas temporales o de datos de aplicación, en lugar de `Program Files` o `System32`).

#### 20.5 DNS a dominio malicioso

```
index=dns sourcetype=dns
| lookup threat_domains.csv query AS domain OUTPUT threat_type confidence
| where isnotnull(threat_type)
| stats count values(threat_type) AS threat_type values(confidence) AS confidence BY src_ip domain
```

**Qué hace:** cruza las consultas DNS con un lookup de inteligencia de amenazas para detectar resoluciones hacia dominios conocidos como maliciosos.

***

### 21. Optimización avanzada

#### 21.1 Usa ventanas temporales concretas

Bien:

```
earliest=-1h latest=now
```

Mejor para alertas (deja margen para la latencia de ingesta):

```
earliest=-15m@m latest=-5m@m
```

Evita usar **“All time”**, ya que obliga a Splunk a escanear todo el histórico disponible.

#### 21.2 Define índice y sourcetype

Bien:

```
index=windows sourcetype=WinEventLog:Security EventCode=4625
```

Mal:

```
EventCode=4625
```

La documentación oficial recomienda conocer bien los índices (indexes), fuentes (sources) y sourcetypes disponibles, y especificar `index`, `source` o `sourcetype` siempre que sea posible para acotar el volumen de datos a escanear.

#### 21.3 Evita wildcards iniciales

Mal:

```
user=*admin*
```

Mejor:

```
user=admin*
```

Mejor aún si puedes normalizar el dato en origen o con un campo calculado:

```
user_type=admin
```

#### 21.4 Usa campos indexados

Mejor (filtro en búsqueda base, indexable):

```
index=proxy sourcetype=bluecoat action=blocked
```

Peor (filtro posterior con `where`):

```
index=proxy
| where action="blocked"
```

#### 21.5 Usa `fields` para reducir el volumen de datos en la tubería

```
| fields _time user src_ip action
```

#### 21.6 Usa `stats` en vez de `transaction`

Mal:

```
index=web
| transaction session_id
```

Mejor:

```
index=web
| stats earliest(_time) AS start latest(_time) AS end values(action) AS actions BY session_id
| eval duration=end-start
```

#### 21.7 Usa `lookup` en vez de `join`

Mal:

```
| join user [ search index=identity | fields user department ]
```

Mejor:

```
| lookup identity.csv user OUTPUT department
```

#### 21.8 Usa `tstats` y data models acelerados

```
| tstats summariesonly=true count FROM datamodel=Authentication.Authentication
  WHERE Authentication.action=failure
  BY Authentication.user Authentication.src
```

#### 21.9 Evita `sort 0` salvo necesidad real

Mal:

```
| sort 0 - _time
```

Mejor:

```
| sort - _time
| head 100
```

#### 21.10 Filtra antes de extraer con regex

Mal:

```
index=app
| rex field=_raw "user=(?<user>[^\s]+)"
| search error
```

Mejor:

```
index=app error
| rex field=_raw "user=(?<user>[^\s]+)"
```

#### 21.11 Evita `mvexpand` temprano

Mal:

```
index=cloud
| mvexpand resources
| search resource_type=vm
```

Mejor:

```
index=cloud resource_type=vm
| mvexpand resources
```

***

### 22. Comparativa de comandos parecidos

#### `search` vs `where`

Usa `search` para filtros simples e indexables:

```
index=web status=500
```

Usa `where` para expresiones que requieren evaluación (funciones, comparaciones entre campos calculados, regex, etc.):

```
| where bytes > avg_bytes * 3
```

#### `fields` vs `table`

* `fields`: controla qué campos siguen disponibles en el **pipeline** para procesamiento posterior.

```
| fields user src_ip
```

* `table`: da formato a la **salida final** visual.

```
| table user src_ip count
```

#### `stats` vs `eventstats`

* `stats`: **colapsa** los eventos en filas de resumen (se pierden los eventos individuales).

```
| stats count BY user
```

* `eventstats`: calcula la estadística pero **mantiene** todos los eventos originales, añadiendo el valor calculado como un nuevo campo en cada evento.

```
| eventstats count AS user_count BY user
```

#### `eventstats` vs `streamstats`

* `eventstats`: calcula el valor de forma **global** (usando todo el conjunto de resultados) y lo añade a cada evento.

```
| eventstats avg(bytes) AS avg_bytes BY user
```

* `streamstats`: calcula el valor de forma **progresiva/secuencial**, evento a evento, según el orden en que aparecen.

```
| streamstats count BY user
```

#### `values()` vs `list()`

* `values()`: valores **únicos**, en orden lexicográfico (alfabético), no necesariamente el orden original de aparición.

```
| stats values(src_ip) BY user
```

* `list()`: lista de valores que **conserva la secuencia relativa** de los eventos según la documentación oficial (incluye repetidos, limitado a 100 valores).

```
| stats list(src_ip) BY user
```

Ejemplo comparativo: si las IPs vistas son `1.1.1.1`, `1.1.1.1`, `2.2.2.2`:

* `values()` devuelve: `1.1.1.1`, `2.2.2.2` (únicos).
* `list()` devuelve: `1.1.1.1`, `1.1.1.1`, `2.2.2.2` (con repetidos, en orden de aparición).

#### `dedup` vs `stats latest`

* `dedup`:

```
| dedup user
```

* `stats` con `latest`:

```
| stats latest(_time) AS last_seen latest(action) AS last_action BY user
```

`stats` con `latest()` suele ser más explícito y controlable que `dedup` cuando el objetivo es “quedarme con el evento más reciente por entidad”.

#### `first()`/`last()` vs `earliest()`/`latest()`

* `first()`/`last()`: dependen del **orden actual** de los resultados en el pipeline (que puede haber sido alterado por un `sort` previo).

```
| stats first(action) AS first_action last(action) AS last_action BY user
```

* `earliest()`/`latest()`: dependen estrictamente del **campo de tiempo** (`_time`) del evento, independientemente del orden de procesamiento.

```
| stats earliest(action) AS first_action latest(action) AS last_action BY user
```

**Recomendación:** para eventos temporales (la inmensa mayoría de los casos en seguridad), usa `earliest`/`latest`. Usa `first`/`last` solo cuando el orden ya esté controlado explícitamente por ti (por ejemplo, tras un `sort` deliberado).

***

### 23. Patrones de SPL por caso de uso

#### 23.1 Conteo simple

```
index=<index> sourcetype=<sourcetype>
| stats count
```

#### 23.2 Conteo por entidad

```
index=<index>
| stats count BY user
```

#### 23.3 Top N

```
index=<index>
| top limit=10 user
```

#### 23.4 Tendencia temporal

```
index=<index>
| timechart span=1h count
```

#### 23.5 Baseline simple (línea base estadística)

```
index=proxy earliest=-7d@d latest=now
| bin _time span=1h
| stats count AS hourly_count BY user _time
| eventstats avg(hourly_count) AS avg_count stdev(hourly_count) AS stdev_count BY user
| where hourly_count > avg_count + (3 * stdev_count)
```

**Qué hace:** calcula el volumen normal por hora y por usuario durante 7 días, y marca como anómalas las horas donde el volumen supera la media más 3 desviaciones estándar (regla estadística de las “3 sigma”).

#### 23.6 Detección por umbral

```
index=auth action=failure earliest=-15m@m latest=-5m@m
| stats count AS failures BY user
| where failures >= 10
```

#### 23.7 Detección por rareza

```
index=process
| stats count BY process_name
| eventstats avg(count) AS avg stdev(count) AS stdev
| where count < avg - stdev
```

#### 23.8 Enriquecimiento

```
index=auth
| lookup identity.csv user OUTPUT department title privileged
| where privileged="true"
```

#### 23.9 Normalización

```
index=auth
| eval user=lower(coalesce(user, username, account_name))
| eval src_ip=coalesce(src_ip, source_ip, client_ip)
```

***

### 24. SPL para dashboards

#### Panel de volumen

```
index=proxy earliest=-24h
| timechart span=1h count
```

#### Panel top usuarios

```
index=proxy earliest=-24h
| top limit=10 user
```

#### Panel de errores

```
index=app earliest=-24h log_level=ERROR
| stats count BY service error_code
| sort - count
```

#### Panel geográfico

```
index=vpn action=success
| iplocation src_ip
| geostats count BY Country
```

***

### 25. SPL para alertas

#### Reglas recomendadas

* Usar una ventana de tiempo **cerrada** (con `earliest` y `latest` explícitos).
* Dejar un **offset de latencia** (por ejemplo, `latest=-5m@m` en vez de `latest=now`).
* Agrupar por la **entidad relevante** (usuario, host, IP) para que la alerta sea accionable.
* Usar **throttling** para no generar alertas repetidas para la misma entidad en poco tiempo.
* Incluir **campos de investigación** en la salida (IPs, hosts, comandos, etc.) para que el analista no tenga que volver a buscar manualmente.
* **No** usar `now` como `latest` si hay latencia de ingesta importante.
* Evitar alertas con ventana **“All time”**.
* Evitar detecciones sin `index` definido.
* Evitar resultados sin una entidad clara sobre la que actuar.

#### Ejemplo

```
index=windows sourcetype=WinEventLog:Security EventCode=4625 earliest=-15m@m latest=-5m@m
| stats count AS failed_logons values(src_ip) AS src_ips BY user
| where failed_logons >= 10
| eval detection_name="Multiple Failed Logons"
| table detection_name user failed_logons src_ips
```

#### Scheduling recomendado

```
Earliest: -15m@m
Latest:   -5m@m
Cron:     */10 * * * *
Throttle: 1h by user
```

***

### 26. SPL para Splunk Enterprise Security

#### 26.1 Usar data models acelerados

```
| tstats summariesonly=true count FROM datamodel=Authentication.Authentication
  WHERE Authentication.action=failure
  BY Authentication.user Authentication.src
```

`summariesonly=true` indica a Splunk que use **solo** los datos ya resumidos por la aceleración del data model (más rápido, pero puede no incluir los datos más recientes que aún no se han resumido).

#### 26.2 Campos CIM

Ejemplo del dataset **Authentication**:

```
Authentication.user
Authentication.src
Authentication.dest
Authentication.action
Authentication.app
```

Ejemplo del dataset **Endpoint (Processes)**:

```
Processes.user
Processes.process_name
Processes.process
Processes.parent_process_name
Processes.dest
```

#### 26.3 SPL “notable-friendly” (preparado para generar notables en ES)

```
| eval risk_object=user
| eval risk_object_type="user"
| eval mitre_tactic="Credential Access"
| eval mitre_technique="Brute Force"
```

Estos campos (`risk_object`, `risk_object_type`, `mitre_tactic`, `mitre_technique`) son convenciones usadas en Splunk Enterprise Security para alimentar el motor de riesgo (Risk-Based Alerting) y facilitar el mapeo a MITRE ATT\&CK dentro de los notables generados.

***

### 27. Buenas prácticas de naming

#### Campos

Usa nombres claros y descriptivos:

```
failed_logons
unique_sources
first_seen
last_seen
src_ips
destinations
```

Evita nombres ambiguos o genéricos:

```
count1
field2
x
tmp
```

#### Detecciones

Nombre recomendado (descriptivo, orientado a la acción/patrón detectado):

```
Multiple Failed Logons From Single User
```

Evitar nombres vagos:

```
Failed login thing
```

#### Alias

```
| stats count AS failed_logons BY user
```

***

### 28. Errores comunes

#### 28.1 No definir índice

Mal:

```
error
```

Bien:

```
index=app error
```

#### 28.2 Usar `where` para filtros simples

Mal:

```
index=web
| where status=500
```

Bien:

```
index=web status=500
```

#### 28.3 Usar `join` innecesario

Mal:

```
| join user [...]
```

Bien:

```
| lookup identity.csv user OUTPUT department
```

#### 28.4 Usar `transaction` para todo

Mal:

```
| transaction user
```

Bien:

```
| stats earliest(_time) latest(_time) values(action) BY user session_id
```

#### 28.5 Ordenar demasiado pronto

Mal:

```
index=proxy
| sort 0 - _time
| stats count BY user
```

Bien:

```
index=proxy
| stats count BY user
| sort - count
```

#### 28.6 Extraer regex antes de filtrar

Mal:

```
index=app
| rex ...
| search error
```

Bien:

```
index=app error
| rex ...
```

#### 28.7 No usar paréntesis con `AND`/`OR` mezclados

Mal:

```
index=web status=401 OR status=403 method=POST
```

Bien:

```
index=web (status=401 OR status=403) method=POST
```

#### 28.8 Wildcard al inicio

Mal:

```
index=auth user=*admin*
```

Mejor:

```
index=auth user=admin*
```

#### 28.9 Comparar strings y números sin convertir

Mal (si `status` viene como string, la comparación numérica puede no funcionar como se espera):

```
| where status > 400
```

Bien:

```
| eval status_num=tonumber(status)
| where status_num > 400
```

#### 28.10 No controlar valores nulos antes de agregar

Mal:

```
| stats count BY user
```

Si hay usuarios vacíos/nulos, mejor:

```
| eval user=coalesce(user,"unknown")
| stats count BY user
```

#### 28.11 Usar `if()` anidado para muchas condiciones

Mal:

```
| eval severity=if(count>100,"critical",if(count>50,"high",if(count>10,"medium","low")))
```

Mejor (más legible y mantenible):

```
| eval severity=case(
    count>100,"critical",
    count>50,"high",
    count>10,"medium",
    true(),"low"
)
```

#### 28.12 Usar `list()` cuando en realidad quieres valores únicos

Mal:

```
| stats list(src_ip) AS src_ips BY user
```

Si solo quieres IPs únicas:

```
| stats values(src_ip) AS src_ips BY user
```

#### 28.13 No normalizar mayúsculas/minúsculas

Mal:

```
| stats count BY user
```

Mejor:

```
| eval user=lower(user)
| stats count BY user
```

#### 28.14 Agrupar por demasiadas dimensiones de alta cardinalidad

Mal:

```
| stats count BY user src_ip host dest_ip process
```

Esto genera una **cardinalidad enorme** (potencialmente millones de combinaciones únicas), lo que dispara el consumo de memoria del search head y puede hacer que la búsqueda falle o tarde muchísimo.

Mejor: reduce las dimensiones a las estrictamente necesarias para la detección, y usa `values()`/`dc()` para conservar el detalle sin explotar la cardinalidad de agrupación:

```
| stats count dc(src_ip) AS unique_sources values(host) AS hosts values(dest_ip) AS dest_ips BY user process
```

#### 28.15 `transaction` sobre volúmenes masivos

Mal:

```
index=web earliest=-24h
| transaction user
```

Sobre cientos de millones de eventos, `transaction` puede consumir toda la memoria disponible del search head e incluso provocar que la búsqueda sea abortada.

Mejor: sustituir por `stats`/`streamstats`, que sí se pueden distribuir parcialmente y son mucho más predecibles en consumo de recursos:

```
index=web earliest=-24h
| stats earliest(_time) AS start latest(_time) AS end count AS events BY user
```

#### 28.16 `join` sobre datasets masivos

Mal:

```
index=auth earliest=-24h
| join user [ search index=identity ]
```

`join` ejecuta la subbúsqueda de forma limitada (por defecto un máximo de resultados) y la trae completa a memoria en el search head; sobre datasets grandes esto trunca resultados silenciosamente o degrada mucho el rendimiento.

Mejor: usar `lookup` (si el dataset de la derecha es relativamente estático) o `stats`/`append` combinados con claves comunes:

```
index=auth earliest=-24h
| lookup identity.csv user OUTPUT department
```

#### 28.17 `values()`/`list()` sobre campos de altísima cardinalidad

Mal:

```
index=endpoint earliest=-24h
| stats values(command_line) AS commands BY host
```

Si `command_line` tiene millones de valores distintos por host, el campo multivalor resultante puede volverse enorme, ralentizar la renderización de la tabla y complicar la lectura para el analista.

Mejor: acotar el volumen antes de agregar (filtrando por patrones sospechosos primero) o limitar el número de valores mostrados:

```
index=endpoint earliest=-24h ("encodedcommand" OR "downloadstring" OR "iex")
| stats count dc(command_line) AS unique_commands values(command_line) AS sample_commands BY host
```

***

### 29. Plantillas listas para usar

#### 29.1 Plantilla de investigación

```
index=<index> sourcetype=<sourcetype> <condicion> earliest=-24h latest=now
| fields _time host user src_ip dest_ip action status _raw
| sort - _time
```

#### 29.2 Plantilla de estadística

```
index=<index> sourcetype=<sourcetype> earliest=-24h latest=now
| stats count AS total_events dc(user) AS unique_users BY action
| sort - total_events
```

#### 29.3 Plantilla de alerta

```
index=<index> sourcetype=<sourcetype> earliest=-15m@m latest=-5m@m
| stats count AS event_count values(src_ip) AS src_ips BY user
| where event_count >= <threshold>
| table user event_count src_ips
```

#### 29.4 Plantilla de baseline

```
index=<index> sourcetype=<sourcetype> earliest=-7d@d latest=now
| bin _time span=1h
| stats count AS current_count BY entity _time
| eventstats avg(current_count) AS avg_count stdev(current_count) AS stdev_count BY entity
| where current_count > avg_count + (3 * stdev_count)
```

#### 29.5 Plantilla con lookup

```
index=<index> sourcetype=<sourcetype> earliest=-24h
| lookup <lookup>.csv <field> OUTPUT <enrichment_field>
| where isnotnull(<enrichment_field>)
| stats count values(<enrichment_field>) AS enrichment BY <field>
```

#### 29.6 Plantilla con CIM/tstats

```
| tstats summariesonly=true count
  FROM datamodel=<DataModel>.<Dataset>
  WHERE <Dataset>.<field>=<value>
  BY <Dataset>.<entity> _time span=1h
```

#### 29.7 Plantilla de detección completa (estructura ideal)

```
index=<index> sourcetype=<sourcetype> <filtros_base> earliest=-15m@m latest=-5m@m
| fields _time host user src_ip dest_ip action signature
| eval user=lower(user)
| lookup identity.csv user OUTPUT department privileged
| stats
    count AS event_count
    values(src_ip) AS src_ips
    values(dest_ip) AS dest_ips
    values(action) AS actions
    earliest(_time) AS first_seen
    latest(_time) AS last_seen
  BY user privileged
| where event_count >= <threshold>
| eval first_seen=strftime(first_seen,"%Y-%m-%d %H:%M:%S")
| eval last_seen=strftime(last_seen,"%Y-%m-%d %H:%M:%S")
| table user privileged event_count src_ips dest_ips actions first_seen last_seen
```

***

### 30. Referencia completa de categorías de comandos

#### Búsqueda y filtrado

```
search
where
regex
fields
table
head
tail
dedup
sort
```

#### Transformación

```
stats
chart
timechart
top
rare
eventstats
streamstats
xyseries
untable
transpose
```

#### Evaluación y manipulación

```
eval
fieldformat
rename
replace
convert
fillnull
filldown
```

#### Extracción

```
rex
spath
extract
kv
xmlkv
```

#### Multivalue

```
makemv
mvexpand
nomv
mvcombine
```

#### Enriquecimiento

```
lookup
inputlookup
outputlookup
iplocation
```

#### Correlación

```
transaction
join
append
appendcols
selfjoin
set
```

#### Generación

```
makeresults
inputlookup
tstats
mstats
metadata
datamodel
savedsearch
```

#### Avanzado

```
map
collect
sendemail
addinfo
addtotals
addcoltotals
accum
autoregress
delta
predict
anomalies
anomalousvalue
cluster
```

***

### 31. Referencia práctica de funciones

#### Condicionales

```
if
case
true
false
validate
```

#### Nulos

```
isnull
isnotnull
null
coalesce
```

#### String

```
lower
upper
len
substr
replace
trim
ltrim
rtrim
split
mvjoin
like
match
```

#### Matemáticas

```
abs
round
floor
ceil
sqrt
pow
exp
ln
log
pi
random
tonumber
tostring
```

#### Tiempo

```
now
time
relative_time
strftime
strptime
```

#### Red/IP

```
cidrmatch
ipmask
```

#### Multivalue

```
mvappend
mvcount
mvdedup
mvfilter
mvfind
mvindex
mvjoin
mvmap
mvrange
mvsort
mvzip
split
```

#### Estadísticas

```
count
dc
distinct_count
estdc
estdc_error
sum
avg
min
max
median
mode
range
stdev
stdevp
var
varp
perc
exactperc
upperperc
earliest
latest
first
last
list
values
```

***

### 32. Tablas rápidas de referencia

#### 32.1 Tabla de operadores

| Operador | Sirve para              | Ejemplo                         |
| -------- | ----------------------- | ------------------------------- |
| `=`      | Igualdad                | `status=200`                    |
| `!=`     | Diferente               | `status!=200`                   |
| `>`      | Mayor que               | `count>10`                      |
| `<`      | Menor que               | `duration<5`                    |
| `>=`     | Mayor o igual           | `failures>=10`                  |
| `<=`     | Menor o igual           | `score<=50`                     |
| `AND`    | Ambas condiciones       | `user=admin AND action=failure` |
| `OR`     | Una u otra              | `status=401 OR status=403`      |
| `NOT`    | Excluir                 | `NOT uri="/health"`             |
| `XOR`    | Una condición exclusiva | `a XOR b`                       |
| `+`      | Sumar                   | `a+b`                           |
| `-`      | Restar                  | `a-b`                           |
| `*`      | Multiplicar             | `a*b`                           |
| `/`      | Dividir                 | `a/b`                           |
| `%`      | Resto                   | `a%2`                           |
| `.`      | Concatenar texto        | `"user=".user`                  |
| `IN`     | Está en lista           | `status IN (401,403)`           |
| `()`     | Agrupar lógica          | `(a OR b) AND c`                |
| `*`      | Wildcard                | `user=admin*`                   |

#### 32.2 Tabla de funciones de evaluación

| Función           | Sirve para           | Ejemplo                               |
| ----------------- | -------------------- | ------------------------------------- |
| `if()`            | Condición simple     | `if(count>10,"high","low")`           |
| `case()`          | Varias condiciones   | `case(a>10,"high",true(),"low")`      |
| `true()`          | Default en `case()`  | `true(),"other"`                      |
| `false()`         | Valor falso          | `false()`                             |
| `validate()`      | Validar campos       | `validate(isnotnull(user),"missing")` |
| `isnull()`        | Campo nulo           | `isnull(user)`                        |
| `isnotnull()`     | Campo no nulo        | `isnotnull(user)`                     |
| `null()`          | Crear null           | `null()`                              |
| `nullif()`        | Null si coincide     | `nullif(user,"-")`                    |
| `coalesce()`      | Primer valor no nulo | `coalesce(user,username)`             |
| `lower()`         | Minúsculas           | `lower(user)`                         |
| `upper()`         | Mayúsculas           | `upper(user)`                         |
| `len()`           | Longitud             | `len(user)`                           |
| `substr()`        | Substring            | `substr(user,1,3)`                    |
| `replace()`       | Reemplazar con regex | `replace(user,"@x.com","")`           |
| `trim()`          | Quitar espacios      | `trim(user)`                          |
| `ltrim()`         | Quitar izquierda     | `ltrim(user)`                         |
| `rtrim()`         | Quitar derecha       | `rtrim(user)`                         |
| `split()`         | Dividir string       | `split(email,"@")`                    |
| `match()`         | Regex true/false     | `match(user,"^adm")`                  |
| `like()`          | Patrón simple        | `like(url,"%admin%")`                 |
| `round()`         | Redondear            | `round(bytes/1024,2)`                 |
| `floor()`         | Redondear abajo      | `floor(value)`                        |
| `ceil()`          | Redondear arriba     | `ceil(value)`                         |
| `abs()`           | Valor absoluto       | `abs(a-b)`                            |
| `sqrt()`          | Raíz cuadrada        | `sqrt(value)`                         |
| `pow()`           | Potencia             | `pow(value,2)`                        |
| `tonumber()`      | A número             | `tonumber(status)`                    |
| `tostring()`      | A string             | `tostring(status)`                    |
| `now()`           | Tiempo actual        | `now()`                               |
| `relative_time()` | Tiempo relativo      | `relative_time(now(),"-1h")`          |
| `strftime()`      | Epoch a texto        | `strftime(_time,"%Y-%m-%d")`          |
| `strptime()`      | Texto a epoch        | `strptime(date,"%Y-%m-%d")`           |
| `cidrmatch()`     | IP en red            | `cidrmatch("10.0.0.0/8",src_ip)`      |
| `ipmask()`        | Aplicar máscara      | `ipmask("255.255.255.0",src_ip)`      |
| `md5()`           | Hash MD5             | `md5(user)`                           |
| `sha1()`          | Hash SHA1            | `sha1(value)`                         |
| `sha256()`        | Hash SHA256          | `sha256(value)`                       |
| `isnum()`         | Es número            | `isnum(bytes)`                        |
| `isstr()`         | Es string            | `isstr(user)`                         |
| `isint()`         | Es entero            | `isint(status)`                       |
| `typeof()`        | Tipo de dato         | `typeof(field)`                       |

#### 32.3 Tabla de funciones estadísticas

| Función            | Sirve para               | Ejemplo                       |
| ------------------ | ------------------------ | ----------------------------- |
| `count`            | Contar eventos           | `stats count BY user`         |
| `count(field)`     | Contar eventos con campo | `stats count(user)`           |
| `dc()`             | Contar únicos            | `stats dc(src_ip)`            |
| `distinct_count()` | Igual que `dc()`         | `stats distinct_count(user)`  |
| `estdc()`          | Únicos estimados         | `stats estdc(user)`           |
| `estdc_error()`    | Error estimado           | `stats estdc_error(user)`     |
| `sum()`            | Sumar                    | `stats sum(bytes)`            |
| `avg()`            | Media                    | `stats avg(duration)`         |
| `min()`            | Mínimo                   | `stats min(_time)`            |
| `max()`            | Máximo                   | `stats max(_time)`            |
| `range()`          | Max - min                | `stats range(_time)`          |
| `median()`         | Mediana                  | `stats median(duration)`      |
| `mode()`           | Valor más frecuente      | `stats mode(country)`         |
| `stdev()`          | Desviación estándar      | `stats stdev(bytes)`          |
| `stdevp()`         | Desviación poblacional   | `stats stdevp(bytes)`         |
| `var()`            | Varianza                 | `stats var(bytes)`            |
| `varp()`           | Varianza poblacional     | `stats varp(bytes)`           |
| `perc95()`         | Percentil 95 aproximado  | `stats perc95(duration)`      |
| `exactperc95()`    | Percentil exacto         | `stats exactperc95(duration)` |
| `upperperc95()`    | Percentil superior       | `stats upperperc95(duration)` |
| `earliest()`       | Valor más antiguo        | `stats earliest(src_ip)`      |
| `latest()`         | Valor más reciente       | `stats latest(src_ip)`        |
| `first()`          | Primer valor por orden   | `stats first(action)`         |
| `last()`           | Último valor por orden   | `stats last(action)`          |
| `values()`         | Valores únicos           | `stats values(src_ip)`        |
| `list()`           | Lista con orden          | `stats list(action)`          |
| `sparkline()`      | Mini tendencia           | `stats sparkline(count)`      |

#### 32.4 Tabla de funciones multivalue

| Función      | Sirve para          | Ejemplo                         |
| ------------ | ------------------- | ------------------------------- |
| `mvappend()` | Crear multivalue    | `mvappend(src_ip,dest_ip)`      |
| `mvcount()`  | Contar valores      | `mvcount(src_ips)`              |
| `mvdedup()`  | Quitar duplicados   | `mvdedup(src_ips)`              |
| `mvfilter()` | Filtrar valores     | `mvfilter(match(users,"^adm"))` |
| `mvfind()`   | Buscar posición     | `mvfind(users,"admin")`         |
| `mvindex()`  | Sacar posición      | `mvindex(values,0)`             |
| `mvjoin()`   | Multivalue a string | `mvjoin(src_ips,",")`           |
| `mvmap()`    | Aplicar función     | `mvmap(users,lower(users))`     |
| `mvrange()`  | Crear rango         | `mvrange(1,10,1)`               |
| `mvsort()`   | Ordenar valores     | `mvsort(src_ips)`               |
| `mvzip()`    | Combinar listas     | `mvzip(users,ips,"=")`          |

***

### 33. Ejemplo final completo y comentado

```
index=windows sourcetype=WinEventLog:Security EventCode IN (4624,4625) earliest=-30m@m latest=-5m@m
| fields _time host user src_ip EventCode
| eval user=lower(coalesce(user,"unknown"))
| eval action=case(
    EventCode=4624, "success",
    EventCode=4625, "failure",
    true(), "other"
)
| stats
    count AS total_events
    count(eval(action="failure")) AS failed_logons
    count(eval(action="success")) AS successful_logons
    dc(src_ip) AS unique_sources
    values(src_ip) AS src_ips
    earliest(_time) AS first_seen
    latest(_time) AS last_seen
  BY user host
| where failed_logons >= 10 AND successful_logons >= 1
| eval first_seen=strftime(first_seen,"%Y-%m-%d %H:%M:%S")
| eval last_seen=strftime(last_seen,"%Y-%m-%d %H:%M:%S")
| sort - failed_logons
| table user host failed_logons successful_logons unique_sources src_ips first_seen last_seen
```

#### Qué componentes usa esta query

* **Filtros de búsqueda base:** `index`, `sourcetype`, `EventCode IN (...)`, `earliest`/`latest` con offset.
* **Comandos:** `fields`, `eval`, `stats`, `where`, `sort`, `table`.
* **Operadores:** `IN`, `AND`.
* **Funciones:** `lower()`, `coalesce()`, `case()`, `true()`, `count()`, `dc()`, `values()`, `earliest()`, `latest()`, `strftime()`.

#### Qué detecta

Usuarios que tienen **muchos logins fallidos** (10 o más) y **al menos un login exitoso** durante o después de esa ventana - un patrón clásico de ataque de fuerza bruta que finalmente logró acceso.

***

### 34. Checklist de optimización SPL

Antes de dar una query por buena, revisa:

* [ ] Tiene `index` definido.
* [ ] Tiene `sourcetype`/`source`/`host` si aplica.
* [ ] Tiene una ventana temporal limitada (no “All time”).
* [ ] Filtra lo antes posible.
* [ ] Evita wildcards iniciales (`texto`).
* [ ] Usa campos indexados en la búsqueda base cuando puede.
* [ ] Reduce campos con `fields` tan pronto como sea posible.
* [ ] Evita `join` salvo necesidad real (usa `lookup`/`stats` si aplica).
* [ ] Evita `transaction` salvo necesidad real (usa `stats`/`streamstats` si aplica).
* [ ] Evita `sort 0` antes de agregar datos.
* [ ] Usa `stats` con `earliest`/`latest` antes que `dedup` si busca el primer/último evento por entidad.
* [ ] Usa `lookup` en vez de `join` si hay una tabla de referencia disponible.
* [ ] Usa `tstats` si hay un data model acelerado o campos indexados disponibles.
* [ ] Usa `latest` con offset (por ejemplo `5m@m`) para alertas, absorbiendo la latencia de ingesta.
* [ ] Incluye la entidad relevante (usuario, host, IP) para permitir throttling de alertas.
* [ ] Incluye campos investigables en la salida final (IPs, hosts, comandos, timestamps).

***

### 35. Reglas mentales finales

Cuando escribas SPL, piensa en este orden de preguntas:

1. ¿Qué índice necesito?
2. ¿Qué sourcetype necesito?
3. ¿Qué ventana temporal necesito?
4. ¿Qué filtro simple puedo poner al principio?
5. ¿Qué campos necesito conservar?
6. ¿Qué campos debo crear o normalizar?
7. ¿Tengo que filtrar con `where`?
8. ¿Tengo que agregar con `stats`/`timechart`/`chart`?
9. ¿Tengo que enriquecer con `lookup`?
10. ¿Qué salida final necesita el analista?

Y para elegir funciones, usa esta guía mental rápida:

| Si quieres…                                    | Usa…            |
| ---------------------------------------------- | --------------- |
| Decisión simple entre dos valores              | `if()`          |
| Decisión entre muchos valores                  | `case()`        |
| Primer valor existente entre varios candidatos | `coalesce()`    |
| Normalizar texto a minúsculas                  | `lower()`       |
| Filtrar con expresión regular                  | `match()`       |
| Filtrar con patrón simple (`%`)                | `like()`        |
| Comprobar si una IP pertenece a una red        | `cidrmatch()`   |
| Contar eventos                                 | `count`         |
| Contar valores únicos                          | `dc()`          |
| Guardar valores únicos                         | `values()`      |
| Guardar una secuencia ordenada de valores      | `list()`        |
| Mostrar una fecha legible                      | `strftime()`    |
| Convertir texto a fecha (epoch)                | `strptime()`    |
| Redondear un número                            | `round()`       |
| Trabajar con campos multivalor                 | funciones `mv*` |

#### Reglas heurísticas rápidas (“code smells” de SPL)

* Si la query empieza con `index=... sourcetype=...`, normalmente vas bien encaminado.
* Si empieza con , probablemente está mal planteada.
* Si tiene `join`, pregúntate si puede sustituirse por `lookup` o `stats`.
* Si tiene `transaction`, pregúntate si puede sustituirse por `stats`.
* Si tiene `sort 0`, asegúrate de necesitar absolutamente **todos** los resultados ordenados.
* Si tiene `mvexpand`, revisa cuántas filas va a multiplicar antes de ejecutarla sobre grandes volúmenes.
* Si es una **alerta**, usa `earliest`/`latest` con offset de latencia.
* Si es un **dashboard**, prioriza agregaciones (`stats`, `timechart`) sobre eventos crudos.
* Si es para **Enterprise Security**, usa CIM y `tstats` siempre que sea posible.
* Si es **threat hunting**, empieza con un alcance amplio pero reduce rápido con filtros.
* Si es para **producción**, mide el rendimiento real con el Job Inspector de Splunk.
* Si procesas datos de autenticación, normaliza siempre `user`, `src`, `dest`, `action`.
* Si creas detecciones, devuelve siempre: entidad, evidencia, volumen y tiempo.
* Si usas lookups, controla la cardinalidad (número de filas) para evitar problemas de rendimiento.
* Si usas `rex`, filtra **antes** de extraer.
* Si necesitas ver tendencias, usa `timechart`.
* Si necesitas una tabla final, usa `table` al **final**, nunca al principio.
* Si necesitas seguir procesando campos después, usa `fields`, no `table`.
* Si necesitas búsquedas aceleradas, usa data models y `tstats`.
* Si no sabes si una búsqueda es pesada, probablemente lo es - hasta que la midas con el Job Inspector.

#### Mini chuleta final

```
index=... sourcetype=... earliest=-1h
| fields _time host user src_ip dest_ip action
| eval user=lower(user)
| lookup identity.csv user OUTPUT department
| stats count AS total values(src_ip) AS src_ips BY user department
| where total > 10
| sort - total
| table user department total src_ips
```

**Orden mental resumido:**

1. Buscar poco (índice + sourcetype + tiempo + filtros simples).
2. Filtrar pronto.
3. Calcular solo lo necesario.
4. Agregar cuanto antes.
5. Ordenar al final.
6. Mostrar solo lo útil.

***

### 36. Arquitectura de búsqueda en Splunk

Hasta ahora este documento ha enseñado **qué** escribir en SPL. Esta sección explica **por qué** unas búsquedas son rápidas y otras lentas, entendiendo cómo Splunk mueve y procesa los datos internamente. Sin este contexto, recomendaciones como “usa `tstats`” parecen magia; con él, son consecuencia lógica de la arquitectura.

#### 36.1 Componentes principales

* **Indexer (indexador):** recibe los datos, los parsea, los indexa y los almacena en disco en forma de *buckets*. También es responsable de ejecutar la parte de la búsqueda que se puede paralelizar (filtrado, extracción de campos, agregaciones parciales) sobre los datos que él mismo almacena.
* **Search Head (cabeza de búsqueda):** recibe la query del usuario, la descompone, la distribuye a los indexers relevantes, y luego combina (reduce) los resultados parciales que le devuelven los indexers para producir el resultado final. También ejecuta los comandos que **no** se pueden distribuir (por ejemplo, `sort`, `transaction`, `join`).
* **Forwarder:** agente que recoge los datos en origen y los envía a los indexers (no interviene en la búsqueda, pero es la primera pieza del pipeline de ingesta).

#### 36.2 Buckets: hot, warm, cold, frozen

Los indexers almacenan los datos en **buckets**, agrupados por rango temporal:

| Bucket     | Descripción                                                                                                                                                                                               |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Hot**    | Bucket activo, donde se están escribiendo los datos más recientes. Solo hay uno (o pocos) por índice y por indexer en un momento dado. Es el más rápido de consultar porque suele estar en memoria/caché. |
| **Warm**   | Buckets ya cerrados (no reciben más escrituras) pero todavía en almacenamiento “rápido” (por ejemplo, disco local SSD).                                                                                   |
| **Cold**   | Buckets más antiguos, movidos a almacenamiento más económico (a veces más lento, como almacenamiento en red o barato).                                                                                    |
| **Frozen** | Datos que han superado la retención configurada; se eliminan o se archivan fuera de Splunk (dependiendo de la política).                                                                                  |

**Implicación práctica:** cuanto más amplia sea la ventana temporal de tu búsqueda (`earliest`/`latest`), más buckets (potencialmente hot + warm + cold) tiene que abrir Splunk, lo que explica directamente por qué acotar el tiempo es la optimización con mayor impacto de todas.

#### 36.3 TSIDX: el índice de términos

Cada bucket contiene, además de los datos crudos comprimidos (`rawdata`), un conjunto de archivos **`.tsidx`** (*time series index*). Estos archivos son, conceptualmente, un índice invertido: para cada término (palabra, o valor de un campo indexado como `host`, `source`, `sourcetype`) guardan una lista de en qué eventos aparece y en qué posición temporal.

Esto explica varias reglas que ya se han mencionado antes en este documento:

* Buscar por `index=`, `sourcetype=`, `host=`, o por una keyword exacta, es rápido porque Splunk puede consultar directamente el `.tsidx` sin necesidad de leer y descomprimir el `rawdata`.
* Un wildcard al **inicio** de un término (`*admin`) no puede aprovechar el índice de términos de la misma forma que un wildcard al final (`admin*`), porque el índice está optimizado para buscar por el principio del término.
* `tstats` es rápido precisamente porque **solo lee los `.tsidx`**, nunca el `rawdata`. Por eso solo puede operar sobre campos que estén indexados (campos por defecto como `host`/`source`/`sourcetype`, o campos acelerados vía *Data Model Acceleration*), y no sobre cualquier campo extraído en tiempo de búsqueda.

#### 36.4 Data Model Acceleration (DMA)

Cuando se activa la aceleración de un *data model* en Splunk Enterprise Security (o Splunk Enterprise), Splunk pre-calcula y almacena en disco (en una estructura similar a `.tsidx`) los valores de los campos definidos en ese data model, para un rango de tiempo configurado (por ejemplo, los últimos 90 días).

Esto significa que una consulta `tstats ... FROM datamodel=Authentication.Authentication` no tiene que leer los eventos crudos de `WinEventLog:Security`, `Okta`, `Linux auth`, etc., cada vez: simplemente consulta el resumen ya construido. Es la razón por la que `tstats summariesonly=true` sobre un data model acelerado puede ser órdenes de magnitud más rápido que un `stats` equivalente sobre los eventos crudos.

**Coste:** la aceleración consume espacio en disco adicional (el resumen) y CPU en segundo plano (para mantenerlo actualizado). Es un trade-off clásico: más velocidad en búsqueda a cambio de más recursos en background.

#### 36.5 Summary Index

Un *summary index* es un índice normal de Splunk, pero en el que se guardan **resultados ya agregados** de una búsqueda (por ejemplo, el conteo de eventos por hora y por usuario), generados periódicamente mediante el comando `collect`. A diferencia de la aceleración de data models (que es automática y gestionada por Splunk), un summary index requiere que tú mismo definas y programes la búsqueda que “resume” los datos.

#### 36.6 Search pipeline (fases de una búsqueda distribuida)

De forma simplificada, una búsqueda distribuida en Splunk sigue este flujo:

1. **Parsing:** el search head interpreta la query SPL y determina qué partes se pueden distribuir a los indexers y cuáles deben ejecutarse centralizadamente.
2. **Map (fase distribuida):** los comandos *distributable streaming* (`search`, `where`, `eval`, `fields`, `rename`, parte de `stats`…) se ejecutan en paralelo en cada indexer, sobre los buckets relevantes.
3. **Reduce (fase centralizada):** el search head recibe los resultados parciales de cada indexer y los combina. Aquí se ejecutan los comandos que necesitan ver **todo** el conjunto de resultados a la vez (`sort`, `transaction`, `join`, la fase final de `stats`/`stats`+`BY`, etc.).

Esta es la base conceptual de la sección [38](#id-38.-paralelizacion-que-comandos-rompen-la-distribucion): cuanto más trabajo se pueda dejar en la fase “map” (indexers, en paralelo) y menos en la fase “reduce” (search head, centralizado), más rápida y escalable será la búsqueda.

***

### 37. Search Job Inspector: cómo identificar búsquedas lentas

El **Job Inspector** es la herramienta nativa de Splunk para diagnosticar el rendimiento real de una búsqueda ya ejecutada. Se accede desde el icono de engranaje (“Inspect Job”) en los resultados de cualquier búsqueda. Aprender a leerlo es una habilidad diferencial entre un analista que “escribe SPL que funciona” y uno que “escribe SPL que escala”.

#### 37.1 Métricas clave a revisar

| Métrica               | Qué significa                                                                                          | Qué mirar                                                                       |
| --------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| `scanCount`           | Número de eventos que Splunk tuvo que **leer/escanear** de los buckets para procesar la búsqueda.      | Si es muy alto comparado con `eventCount`, la búsqueda base está mal acotada.   |
| `eventCount`          | Número de eventos que realmente **coincidieron** con la búsqueda base.                                 | Debería ser razonablemente cercano a `scanCount` si los filtros son eficientes. |
| `resultCount`         | Número de filas en el resultado final (después de `stats`, `where`, etc.).                             | Útil para saber si el volumen final es manejable para un dashboard/alerta.      |
| `runDuration`         | Tiempo total (segundos) que tardó la búsqueda completa.                                                | Objetivo a minimizar, sobre todo en alertas programadas frecuentes.             |
| `cpu_seconds`         | Tiempo de CPU consumido por cada comando.                                                              | Permite identificar qué comando concreto es el cuello de botella.               |
| `command.invocations` | Cuántas veces se invocó cada comando (relevante en comandos como `map` que se ejecutan por resultado). | Un número inesperadamente alto aquí es señal de una subbúsqueda costosa.        |

#### 37.2 La red flag más importante: `scanCount` vs `eventCount`

```
scanCount  = 300,000,000
eventCount = 50
```

Esto es una **red flag enorme**: Splunk tuvo que escanear 300 millones de eventos para quedarse solo con 50. Significa que la búsqueda base (`index=`, `sourcetype=`, filtros de campo, ventana de tiempo) **no está filtrando de forma eficiente** y se está dependiendo de un `where`/`search` posterior (o de un campo no indexado) para hacer el trabajo pesado.

**Diagnóstico típico y solución:**

* Si el filtro real está en un `| where` o en un `| search` después del primer pipe → mueve esa condición a la búsqueda base si es un campo indexado o una keyword.
* Si el filtro depende de un campo extraído en tiempo de búsqueda (no indexado) → valora si ese campo se puede convertir en un campo indexado (via `INDEXED_EXTRACTIONS` o similar) o si puede acotarse mejor por `sourcetype`/`source`.
* Si la ventana de tiempo es muy amplia → redúcela o usa `tstats`/data model acelerado.

#### 37.3 Desglose por comando

El Job Inspector muestra también un desglose de tiempo y recursos **por cada comando** de la tubería. Esto permite responder preguntas como:

* ¿Cuánto tiempo se fue en el `lookup`? (posible indicio de un lookup demasiado grande o mal indexado).
* ¿Cuánto tiempo se fue en el `sort`? (posible indicio de que se está ordenando un volumen de datos innecesariamente grande, antes de agregar).
* ¿Cuántas veces se invocó una subbúsqueda de `map`? (si el número es alto, `map` está lanzando muchísimas búsquedas independientes).

#### 37.4 Buena práctica de trabajo

Antes de dar por buena una detección o dashboard que se ejecutará de forma recurrente (alerta programada, panel muy visitado), es buena práctica:

1. Ejecutar la búsqueda con el rango de tiempo real de producción.
2. Abrir el Job Inspector.
3. Verificar que `scanCount` esté en un orden de magnitud razonable respecto a `eventCount`.
4. Revisar qué comando consume más `cpu_seconds` y evaluar si se puede optimizar u ordenar de otra forma.
5. Repetir tras cualquier cambio relevante en la query.

***

### 38. Paralelización: qué comandos rompen la distribución

La sección [6](#id-6.-tipos-de-comandos-spl) ya introdujo las seis categorías generales de comandos. Esta sección profundiza específicamente en la pregunta de rendimiento más importante: **¿qué comandos impiden que Splunk reparta el trabajo entre los indexers?**

#### 38.1 Recordatorio de las categorías relevantes para paralelización

* **Distributable streaming** (`search`, `where`, `eval`, `fields`, `rename`, `rex`, `lookup`): se ejecutan evento a evento y **sí se pueden paralelizar** en cada indexer. Son los más “baratos” de usar temprano en la query.
* **Centralized streaming** (`dedup`, `head`, `tail`, `streamstats`): procesan evento a evento pero **necesitan centralizarse** en el search head (por ejemplo, `streamstats` necesita mantener un orden secuencial global).
* **Transforming** (`stats`, `chart`, `timechart`, `top`, `rare`): tienen una fase que sí se puede distribuir parcialmente (cada indexer calcula agregados parciales) y una fase de combinación final en el search head. Son razonablemente eficientes si se colocan después de filtrar.
* **Dataset processing** (`sort`, `eventstats`, `streamstats`, `transaction`): requieren ver **todo** el conjunto de resultados junto, lo que obliga a centralizar el procesamiento en el search head.

#### 38.2 Comandos que rompen la paralelización (y por qué)

| Comando       | Por qué rompe la paralelización                                                                                                                                                                             | Alternativa recomendada                                                                                                       |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `sort`        | Necesita comparar todas las filas entre sí para determinar el orden global; no se puede “ordenar parcialmente” en cada indexer de forma independiente y luego combinar sin volver a ordenar centralmente.   | Agregar primero con `stats`, y ordenar solo el resultado ya reducido; usar `head`/`tail` para limitar cuánto hay que ordenar. |
| `transaction` | Necesita reconstruir secuencias completas de eventos relacionados (por `session_id`, por ejemplo), lo que implica tener todos los eventos candidatos juntos en un mismo proceso.                            | `stats` con `earliest()`/`latest()`/`values()` agrupando por el identificador de sesión.                                      |
| `join`        | La subbúsqueda se ejecuta de forma independiente y su resultado se trae completo al search head antes de cruzarlo con el resultado principal; además tiene límites de tamaño de resultado.                  | `lookup` (si el dataset de la derecha es relativamente estático) o `stats`/`append` con una clave común.                      |
| `eventstats`  | Aunque puede empezar a calcular parcialmente en indexers, para poder añadir el valor agregado a **cada evento individual** necesita conocer el agregado global final, lo que implica una fase centralizada. | Si el objetivo es simplemente agrupar y no necesitas conservar cada evento individual, usar `stats` (más barato).             |
| `streamstats` | Requiere procesar los eventos en un orden secuencial estricto, por lo que no se puede repartir libremente entre indexers sin coordinación.                                                                  | Si no necesitas el cálculo progresivo (solo un valor final), usar `stats`.                                                    |

#### 38.3 Regla práctica

**Cuanto antes aparezca en tu query un comando de la tabla anterior, más “cara” será la búsqueda**, porque fuerza a Splunk a centralizar datos en el search head antes de haber reducido el volumen. La estrategia recomendada es siempre: **filtra y agrega (`stats`) primero, y deja `sort`/`transaction`/`join`/`eventstats`/`streamstats` para el final**, cuando el volumen de datos ya es pequeño.

***

### 39. Metodología de Detection Engineering

Escribir una query SPL que “detecta algo” es solo una parte del trabajo. Construir una **detección de producción** sostenible requiere un método. Este framework de 11 pasos sirve como checklist para cualquier nueva regla de detección.

#### 1. Hipótesis

Define explícitamente qué comportamiento quieres detectar y por qué es sospechoso. Ejemplo: *“Un usuario con más de 10 logins fallidos en 15 minutos, seguido de un login exitoso, indica un posible ataque de fuerza bruta con éxito.”*

#### 2. Fuente de datos

Identifica qué índice/sourcetype contiene los eventos necesarios para validar la hipótesis, y confirma que **realmente se está ingiriendo** (no asumas que un log existe; verifícalo con una búsqueda exploratoria).

#### 3. Campos necesarios

Determina qué campos son imprescindibles (`user`, `src_ip`, `EventCode`…) y verifica que están disponibles, bien extraídos y con el tipo de dato correcto (string vs número).

#### 4. Normalización

Normaliza los campos para que la detección funcione igual independientemente de la fuente concreta (`lower(user)`, `coalesce(src_ip, source_ip)`, mapeo a CIM si aplica).

#### 5. Correlación

Decide si la detección necesita cruzar varias fuentes o tipos de evento (por ejemplo, fallos + éxito, o autenticación + creación de proceso) y con qué comando (`stats` con `count(eval(...))`, `lookup`, `join` solo si es imprescindible).

#### 6. Umbral

Define el umbral cuantitativo (`failures >= 10`) basándote en datos reales, no en una intuición arbitraria. Esto enlaza directamente con el paso de *tuning*.

#### 7. Tuning (ajuste)

Ejecuta la detección en modo “silencioso” (sin generar alertas reales) durante un periodo, y ajusta el umbral según el volumen real de resultados: demasiados resultados → sube el umbral o añade condiciones; cero resultados → revisa si la lógica o los datos son correctos.

#### 8. Falsos positivos

Identifica y documenta explícitamente qué patrones legítimos podrían disparar la detección (por ejemplo, cuentas de servicio con muchos logins automatizados) y decide cómo excluirlos (lookup de exclusión, condición adicional).

#### 9. Severity (severidad)

Asigna una severidad basada en el impacto potencial y la confianza de la detección, no solo en el volumen de eventos. Una detección de alta confianza pero bajo volumen puede merecer severidad alta igualmente.

#### 10. Risk score

En el contexto de Splunk Enterprise Security, calcula (o contribuye a) un *risk score* para la entidad afectada, en vez de generar únicamente una alerta binaria (ver sección [40](#id-40.-risk-based-alerting-rba)).

#### 11. Mapeo ATT\&CK

Documenta a qué técnica(s) de MITRE ATT\&CK corresponde la detección (ver sección [44](#id-44.-mapeo-att-and-ck-para-detecciones-spl)), lo que facilita el reporting, la cobertura de matriz ATT\&CK y la comunicación con otros equipos.

#### Plantilla de documentación de una detección

```
Nombre:            Multiple Failed Logons Followed By Success
Hipótesis:         Fuerza bruta con éxito final
Fuente de datos:   index=windows sourcetype=WinEventLog:Security
Campos:            user, src_ip, EventCode, _time
Normalización:     user en minúsculas, coalesce de src_ip
Correlación:       stats con count(eval(...)) por EventCode
Umbral:            failures >= 10 AND successes >= 1 en 30 min
Falsos positivos:  cuentas de servicio con reintentos automáticos (excluir vía lookup)
Severidad:         High
Risk score:        +40 al risk_object=user
ATT&CK:            T1110 (Brute Force)
Scheduling:        earliest=-30m@m latest=-5m@m, cron */10 * * * *, throttle 1h by user
```

***

### 40. Risk-Based Alerting (RBA)

El Splunk Enterprise Security moderno se apoya cada vez más en **Risk-Based Alerting (RBA)** en lugar de (o además de) alertas binarias tradicionales. La idea central: en vez de que cada detección individual dispare directamente un notable/alerta, cada detección genera un **evento de riesgo** que se acumula sobre una entidad (usuario, host, IP). Solo cuando el riesgo acumulado supera un umbral se genera un incidente. Esto reduce drásticamente la fatiga de alertas de baja confianza consideradas de forma aislada.

#### 40.1 Conceptos clave

* **Risk Rules (reglas de riesgo):** búsquedas guardadas que, en lugar de generar un notable directamente, generan un *risk event* cuando se cumple una condición (por ejemplo, “PowerShell con `encodedcommand`” suma 20 puntos de riesgo al host).
* **Risk Events (eventos de riesgo):** los registros individuales de riesgo generados por cada risk rule, almacenados típicamente en el índice `risk` (data model *Risk*). Cada uno incluye `risk_object`, `risk_object_type` y `risk_score`.
* **Risk Modifiers:** los campos que describen sobre qué entidad y cuánto riesgo se aplica: `risk_object` (el valor, por ejemplo el nombre de usuario), `risk_object_type` (`user`, `system`, `other`), y `risk_score` (el número de puntos que aporta ese evento).
* **Risk Aggregation (agregación de riesgo):** proceso (normalmente una búsqueda programada) que suma los `risk_score` de una misma entidad a lo largo de una ventana de tiempo, produciendo un riesgo acumulado.
* **Risk Incident Rules (reglas de incidente de riesgo):** condiciones sobre el riesgo agregado (por ejemplo, “riesgo acumulado > 100 en 24h” o “3 o más risk events distintos sobre el mismo usuario”) que, al cumplirse, sí generan un notable/incidente para que un analista lo revise.

#### 40.2 Ejemplo de una risk rule en SPL

```
index=windows sourcetype=WinEventLog:Security EventCode=4625 earliest=-15m@m latest=-5m@m
| stats count AS failed_logons BY user
| where failed_logons >= 10
| eval risk_object=user
| eval risk_object_type="user"
| eval risk_score=40
| eval risk_message="Multiples logins fallidos: ".failed_logons." intentos"
| eval mitre_technique_id="T1110"
| collect index=risk sourcetype=stash
```

**Qué hace:** en vez de crear directamente un notable, esta búsqueda añade 40 puntos de riesgo al usuario afectado, dejando constancia del motivo (`risk_message`) y la técnica ATT\&CK asociada (`mitre_technique_id`). El comando `collect` escribe el evento de riesgo en el índice `risk`, que después alimenta el data model *Risk* usado por Enterprise Security.

#### 40.3 Ejemplo de agregación de riesgo (búsqueda de “riesgo acumulado”)

```
| tstats summariesonly=true sum(All_Risk.calculated_risk_score) AS total_risk
  FROM datamodel=Risk.All_Risk
  WHERE earliest=-24h
  BY All_Risk.risk_object All_Risk.risk_object_type
| where total_risk > 100
| sort - total_risk
```

**Qué hace:** suma el riesgo acumulado de cada entidad en las últimas 24 horas y marca como candidatas a incidente solo las que superan 100 puntos, combinando así múltiples señales de baja confianza individual en una alerta de alta confianza agregada.

#### 40.4 Ventajas del enfoque RBA frente a alertas tradicionales

* Reduce la fatiga de alertas: una única detección de baja confianza no genera ruido por sí sola.
* Permite correlacionar señales débiles de **distintas fuentes** sobre la misma entidad (por ejemplo, PowerShell sospechoso + acceso a recurso sensible + login desde IP rara) sumando su riesgo.
* Facilita la priorización: los analistas pueden ordenar el trabajo por riesgo acumulado, en lugar de tratar todas las alertas por igual.
* Proporciona trazabilidad: cada risk event queda registrado, por lo que se puede auditar exactamente qué contribuyó al riesgo final de una entidad.

***

### 41. Threat Hunting: playbooks prácticos

A diferencia de una detección (que se ejecuta de forma programada y automática), el *threat hunting* es un proceso exploratorio, guiado por hipótesis, en el que el analista itera sobre los datos. Estos son playbooks de arranque rápido para las categorías de hunting más habituales.

#### 41.1 DNS Hunting

Objetivo: encontrar dominios poco comunes que podrían ser infraestructura de mando y control (C2) o exfiltración vía DNS.

```
index=dns earliest=-24h
| rare query
```

Refinamiento típico: excluir dominios corporativos conocidos y centrarse en dominios con patrones sospechosos (longitud, entropía, muchos subdominios):

```
index=dns earliest=-24h
| eval domain_length=len(query)
| eval subdomain_count=mvcount(split(query,"."))
| where domain_length > 40 OR subdomain_count > 5
| stats count BY query
| sort - count
```

#### 41.2 PowerShell Hunting

Objetivo: identificar líneas de comando de PowerShell no vistas habitualmente en el entorno (candidatas a ser maliciosas o, al menos, a requerir revisión).

```
index=windows sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational earliest=-24h
("powershell.exe" OR "pwsh.exe")
| stats values(command_line) AS commands count BY host user
```

Refinamiento con normalización de entropía/longitud para priorizar comandos ofuscados:

```
index=windows earliest=-24h ("powershell.exe" OR "pwsh.exe")
| eval command_length=len(command_line)
| where command_length > 500 OR match(lower(command_line),"encodedcommand|frombase64string")
| stats count values(command_line) AS sample BY host user
```

#### 41.3 LOLBIN Hunting (Living Off the Land Binaries)

Objetivo: detectar el uso de binarios legítimos de Windows frecuentemente abusados para ejecutar código, descargar payloads o evadir detección.

Binarios candidatos a vigilar (lista no exhaustiva, conocida en la comunidad como *LOLBAS*):

```
certutil.exe
mshta.exe
rundll32.exe
regsvr32.exe
wmic.exe
bitsadmin.exe
cscript.exe
wscript.exe
msbuild.exe
installutil.exe
```

```
index=windows sourcetype=Sysmon earliest=-24h
| where match(lower(process_name), "certutil|mshta|rundll32|regsvr32|wmic|bitsadmin|cscript|wscript|msbuild|installutil")
| stats count values(command_line) AS commands values(parent_process) AS parents BY host user process_name
```

Refinamiento habitual: filtrar por argumentos concretos que indican descarga/ejecución remota (por ejemplo, `certutil -urlcache -split -f`, `bitsadmin /transfer`, `regsvr32 /i:http`).

#### 41.4 Beaconing (detección de comunicación periódica hacia C2)

Objetivo: encontrar hosts que se comunican con un mismo destino a intervalos regulares, un patrón típico de malware con “check-in” periódico a su servidor de mando y control.

```
index=proxy earliest=-24h
| bin _time span=5m
| stats count BY src_ip dest_ip _time
| eventstats avg(count) AS avg_count stdev(count) AS stdev_count BY src_ip dest_ip
| stats count AS intervals avg(count) AS avg_per_interval stdev(stdev_count) AS variability BY src_ip dest_ip
| where intervals > 20 AND variability < 2
```

**Qué hace:** agrupa las conexiones por intervalos de 5 minutos, calcula cuán regular es el volumen de conexiones a lo largo del tiempo por par origen-destino, y destaca los pares con **muchos intervalos activos y muy poca variabilidad** - es decir, comunicación sostenida y periódica, la firma típica de beaconing.

Complemento habitual con `timechart` para inspección visual:

```
index=proxy earliest=-24h src_ip=10.0.5.23 dest_ip=203.0.113.50
| timechart span=5m count
```

***

### 42. Data Models en profundidad

Los *data models* de Splunk (especialmente los del **CIM**, Common Information Model) proporcionan una capa de abstracción sobre datos de múltiples fuentes, permitiendo escribir una única búsqueda (o usar `tstats`) que funciona sin importar el fabricante o formato original de los logs, siempre que estén correctamente mapeados al modelo.

#### 42.1 Principales data models del CIM

| Data Model              | Uso típico                                                                                   |
| ----------------------- | -------------------------------------------------------------------------------------------- |
| **Authentication**      | Eventos de login/logout, éxitos y fallos, en cualquier sistema (Windows, Linux, Okta, VPN…). |
| **Endpoint**            | Procesos, servicios, registro, sistema de archivos - telemetría de EDR/Sysmon.               |
| **Network Traffic**     | Conexiones de red, firewalls, proxies.                                                       |
| **Malware**             | Detecciones de antivirus/EDR (firma, acción tomada, archivo).                                |
| **Intrusion Detection** | Alertas de IDS/IPS.                                                                          |
| **Web**                 | Logs de servidores web y proxies (HTTP).                                                     |
| **Change Analysis**     | Cambios en configuración, cuentas, permisos.                                                 |
| **Updates**             | Estado de parcheo y actualizaciones de software.                                             |
| **Vulnerabilities**     | Resultados de escáneres de vulnerabilidades.                                                 |

#### 42.2 Root dataset vs child dataset

* **Root dataset**: el dataset “padre” de un data model (por ejemplo, `Authentication`), que define los campos y restricciones base comunes a todo el modelo.
* **Child dataset**: un dataset más específico que hereda del root y añade restricciones adicionales (por ejemplo, `Authentication.Default_Authentication`, o un child dataset personalizado que filtra solo autenticaciones privilegiadas). Los child datasets permiten reutilizar la definición base sin duplicarla, añadiendo solo la diferencia.

Al escribir `tstats ... FROM datamodel=Authentication.Authentication`, en realidad se está consultando el dataset (root o child) `Authentication` dentro del data model `Authentication`.

#### 42.3 Acceleration (aceleración)

Ya introducida conceptualmente en la sección [36.4](#id-36.4-data-model-acceleration-dma): la aceleración pre-calcula un resumen indexado (tipo `.tsidx`) de los campos del data model para un rango de tiempo configurado. Sin aceleración, `tstats FROM datamodel=...` sigue funcionando, pero recorriendo los eventos crudos en el momento de la búsqueda (mucho más lento).

Parámetros relevantes de la aceleración:

* **Summary range**: cuánto tiempo hacia atrás se mantiene el resumen acelerado (por ejemplo, 90 días).
* **Backfill**: si al activar la aceleración, Splunk reconstruye el resumen para datos históricos ya existentes o solo a partir de ese momento.

#### 42.4 Constraints (restricciones)

Cada dataset de un data model tiene una **restricción base** (una búsqueda SPL) que determina qué eventos crudos pertenecen a ese dataset. Por ejemplo, el dataset `Authentication` puede tener como constraint algo como:

```
tag=authentication
```

Esto significa que solo los eventos correctamente **etiquetados** (a través de CIM data model mapping / *Add-on* correspondiente) como `tag=authentication` entrarán en el data model. Si una fuente de datos nueva no está mapeada al CIM (sin los tags/campos correctos), **no aparecerá** en las búsquedas basadas en el data model, aunque los eventos existan en el índice. Esta es la razón número uno por la que una detección basada en `tstats`/data model “no encuentra nada” a pesar de que los datos sí están en Splunk: falta el mapeo CIM de esa fuente.

***

### 43. Summary Indexing y aceleración

Splunk ofrece varios mecanismos para acelerar consultas repetitivas o costosas. Esta sección los compara para saber cuál usar en cada caso.

#### 43.1 Summary Index

Un **summary index** es un índice normal donde se almacenan, de forma periódica, los **resultados ya agregados** de una búsqueda (por ejemplo, “conteo de eventos por hora y por usuario”), generados con el comando `collect`.

```
index=auth earliest=-1h@h latest=@h
| bin _time span=1h
| stats count BY user _time
| collect index=summary_auth marker="hourly_auth_baseline"
```

Después, las búsquedas que necesiten esa agregación consultan directamente `index=summary_auth`, mucho más rápido que recalcular desde los eventos crudos cada vez.

#### 43.2 Summary Search (Report Acceleration)

La **aceleración de reportes** (*Report Acceleration*) es un mecanismo más automático: Splunk gestiona por sí mismo el resumen acelerado de una búsqueda transformadora guardada (típicamente basada en `stats`/`timechart`), sin que el usuario tenga que gestionar manualmente un `collect`. Se activa desde la configuración del reporte guardado (“Accelerate Report”).

#### 43.3 Data Model Acceleration

Ver sección [36.4](#id-36.4-data-model-acceleration-dma) y [42.3](#id-42.3-acceleration-aceleracion). Acelera un data model completo (no una única búsqueda), lo que beneficia a **todas** las búsquedas que usen `tstats`/`from datamodel` sobre ese modelo.

#### 43.4 Comparativa

| Mecanismo                              | Qué acelera                                               | Gestión                                                          | Cuándo usarlo                                                                                                                       |
| -------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `stats` sobre eventos crudos           | Nada (línea base)                                         | Ninguna                                                          | Volúmenes pequeños/medianos, exploración ad-hoc.                                                                                    |
| **Summary Index** (`collect`)          | Una agregación concreta, definida a medida                | Manual (tú programas la búsqueda que alimenta el índice resumen) | Reportes pesados y muy específicos que se consultan con frecuencia y cuya lógica de agregación es estable.                          |
| **Report Acceleration**                | Una búsqueda transformadora guardada                      | Semi-automática (Splunk gestiona el resumen una vez activado)    | Un reporte/dashboard concreto muy usado, sin necesidad de definir manualmente un summary index.                                     |
| **Data Model Acceleration + `tstats`** | Todo un data model (múltiples fuentes normalizadas a CIM) | Automática (gestionada por Splunk/ES)                            | Detecciones y dashboards de seguridad que reutilizan el mismo data model (Authentication, Endpoint…) en muchas búsquedas distintas. |

**Regla práctica:** si la misma agregación se consulta desde **muchas** detecciones o dashboards distintos, prioriza **Data Model Acceleration**. Si es una agregación **única y muy específica** de un solo reporte, un **summary index** manual puede ser más simple de mantener.

***

### 44. Mapeo ATT\&CK para detecciones SPL

Documentar a qué técnica de [MITRE ATT\&CK](https://attack.mitre.org/) corresponde cada detección es una práctica estándar en Detection Engineering: facilita medir la cobertura real del programa de detección frente a la matriz completa, y estandariza la comunicación entre equipos (SOC, threat intel, red team).

#### 44.1 Tabla de ejemplo (técnica → tipo de query)

| Técnica ATT\&CK | Nombre                                               | Tipo de query SPL típica                                                                                                                      |
| --------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **T1110**       | Brute Force                                          | `stats count BY user` sobre `EventCode=4625`, con umbral de fallos.                                                                           |
| **T1059**       | Command and Scripting Interpreter (PowerShell, etc.) | `match()` sobre `command_line` buscando parámetros de ofuscación/descarga.                                                                    |
| **T1055**       | Process Injection                                    | Eventos de Sysmon (EventCode 8/10) correlando proceso origen/destino con `stats`.                                                             |
| **T1218**       | System Binary Proxy Execution (LOLBIN)               | `match()` sobre `process_name` contra la lista de binarios LOLBAS (ver sección [41.3](#id-41.3-lolbin-hunting-living-off-the-land-binaries)). |
| **T1078**       | Valid Accounts                                       | Correlación de logins exitosos desde ubicaciones/IPs/horarios atípicos (`stats` + `eventstats` para baseline).                                |

#### 44.2 Cómo incorporar el mapeo directamente en la SPL

```
index=windows sourcetype=WinEventLog:Security EventCode=4625 earliest=-15m@m latest=-5m@m
| stats count AS failed_logons BY user
| where failed_logons >= 10
| eval mitre_tactic="Credential Access"
| eval mitre_technique="Brute Force"
| eval mitre_technique_id="T1110"
| table user failed_logons mitre_tactic mitre_technique mitre_technique_id
```

Incluir `mitre_technique_id` (el identificador oficial, por ejemplo `T1110`) además del nombre legible permite cruzar automáticamente las detecciones con herramientas de cobertura ATT\&CK (como el ATT\&CK Navigator) sin depender de coincidencias de texto libre.

#### 44.3 Buenas prácticas de mapeo

* Mapea a nivel de **(sub)técnica**, no solo de táctica: `T1110.001` (Password Guessing) es más específico y útil que solo “Credential Access”.
* Una misma detección puede mapear a **varias técnicas** si el comportamiento observado es ambiguo (por ejemplo, ejecución vía `rundll32` puede ser T1218.011 y, según el contexto, también parte de T1055).
* Mantén el mapeo como **metadato versionado** junto a la detección (no solo en un documento externo), para que no se desactualice cuando la detección cambie.
* **Subtécnica vs. técnica padre según el consumidor final:** este documento usa subtécnicas (`T1110.003`, `T1059.001`, `T1558.003`, `T1048.003`, etc.) porque aportan precisión técnica para quien escribe/tunea la detección. Sin embargo, si el `mitre_technique_id` se va a introducir en una plantilla operativa, un ticket de gestión de casos, o una herramienta de terceros que **solo admite el identificador de técnica sin subtécnica**, normaliza al ID padre antes de cargarlo (`T1110.003` → `T1110`, `T1059.001` → `T1059`, `T1558.003` → `T1558`, `T1048.003` → `T1048`). Mantén la subtécnica completa en la documentación técnica/catálogo interno (sección [48](#id-48.-coverage-engineering)) y aplica esta normalización solo en el punto de integración que lo requiera, para no perder precisión en el origen.

***

### 45. Guía de reducción de falsos positivos

Reducir falsos positivos (tuning) es, en la práctica diaria de un SOC, tan importante como escribir la detección inicial. Un flujo de trabajo recomendado:

#### 1. Validar el dataset

Antes de tocar el umbral, confirma que los datos de entrada son correctos y completos: ¿el campo `user` está bien extraído para todas las fuentes relevantes? ¿Falta algún host o segmento que no está enviando logs? Un falso positivo a veces es en realidad un problema de calidad de datos, no de lógica de detección.

```
index=windows sourcetype=WinEventLog:Security EventCode=4625 earliest=-24h
| stats count BY host
| sort host
```

(sirve para verificar que todos los hosts esperados están reportando)

#### 2. Medir el baseline (línea base)

Antes de fijar un umbral, mide el comportamiento normal durante un periodo representativo (por ejemplo, 7-14 días) y calcula media y desviación estándar, en vez de adivinar un número.

```
index=windows sourcetype=WinEventLog:Security EventCode=4625 earliest=-14d@d latest=now
| bin _time span=1d
| stats count AS daily_failures BY user _time
| stats avg(daily_failures) AS avg_failures stdev(daily_failures) AS stdev_failures BY user
```

#### 3. Añadir excepciones

Documenta y excluye explícitamente los patrones legítimos conocidos (cuentas de servicio, escáneres de seguridad autorizados, jobs automatizados) usando un lookup de exclusiones en vez de hardcodear nombres dentro de la query.

```
index=windows sourcetype=WinEventLog:Security EventCode=4625 earliest=-15m@m latest=-5m@m
| lookup service_accounts.csv user OUTPUT is_service_account
| where isnull(is_service_account)
| stats count AS failed_logons BY user
| where failed_logons >= 10
```

#### 4. Añadir criticidad de activos (asset criticality)

No todos los hosts/usuarios tienen el mismo impacto si se ven comprometidos. Enriquecer con criticidad permite ajustar la severidad (y el umbral de tolerancia) según el activo afectado, en vez de aplicar la misma regla rígida a todo el entorno.

```
| lookup asset_inventory.csv host OUTPUT criticality
| eval severity=case(
    criticality="critical" AND failed_logons>=5, "high",
    criticality="critical", "medium",
    failed_logons>=20, "high",
    true(), "low"
)
```

#### 5. Añadir contexto IAM/identidad

Cruzar con datos de identidad (departamento, si la cuenta está deshabilitada, si es una cuenta privilegiada) reduce falsos positivos al distinguir, por ejemplo, un fallo de login de un usuario activo normal frente a una cuenta ya deshabilitada (que no debería estar generando intentos en absoluto - lo cual, de hecho, puede ser aún más sospechoso).

```
| lookup identity.csv user OUTPUT department account_status privileged
| where account_status="active"
```

#### 6. Aplicar Risk-Based Alerting

Como último nivel de reducción de ruido, en vez de generar un notable directamente desde una única detección de confianza media, aplica el modelo RBA (sección [40](#id-40.-risk-based-alerting-rba)): que la detección aporte riesgo a la entidad, y que el incidente solo se genere cuando el riesgo acumulado (posiblemente de varias detecciones distintas) supere un umbral. Esto es especialmente útil para detecciones que, de forma aislada, tienen una tasa de falsos positivos moderada pero que, combinadas con otras señales, son mucho más fiables.

***

### 46. Common SOC Data Sources

Esta sección aterriza todo lo anterior en las fuentes de datos que un analista SOC o Detection Engineer maneja el 90% del tiempo. Para cada una: qué es, campos clave, casos de uso típicos y detecciones habituales.

#### 46.1 Windows Security (WinEventLog:Security)

* **Qué es:** el log de seguridad nativo de Windows (autenticación, gestión de cuentas, políticas de auditoría).
* **Campos clave:** `EventCode`, `user`/`Account_Name`, `src_ip`/`IpAddress`, `Logon_Type`, `dest`/`ComputerName`, `Group_Name` (4728/4732), `Target_User_Name`.
* **Códigos de evento habituales:** `4624` (login exitoso), `4625` (login fallido), `4634`/`4647` (logoff), `4720` (creación de usuario), `4728`/`4732`/`4756` (adición a grupo privilegiado), `4738` (cambio de cuenta), `4771` (Kerberos pre-auth failed), `4769` (petición de ticket de servicio Kerberos, clave para Kerberoasting).
* **Casos de uso:** fuerza bruta, movimiento lateral, escalada de privilegios, persistencia vía cuentas.
* **Detección típica:**

```
index=windows sourcetype=WinEventLog:Security EventCode=4625 earliest=-15m@m latest=-5m@m
| stats count AS failed_logons BY user
| where failed_logons >= 10
```

#### 46.2 Sysmon (Microsoft-Windows-Sysmon/Operational)

* **Qué es:** telemetría de endpoint enriquecida (procesos, red, registro, ficheros) generada por Sysinternals Sysmon.
* **Campos clave:** `Image` (ruta del ejecutable), `CommandLine`, `ParentImage`, `ParentCommandLine`, `Hashes`, `DestinationIp`, `DestinationPort`, `TargetObject` (registro).
* **EventCodes habituales:** `1` (creación de proceso), `3` (conexión de red), `7` (carga de módulo/DLL), `8` (creación remota de hilo - inyección), `10` (acceso a proceso), `11` (creación de fichero), `12`/`13`/`14` (operaciones de registro), `22` (consulta DNS).
* **Casos de uso:** detección de malware, LOLBIN, inyección de procesos, persistencia vía registro.
* **Detección típica:** ver PowerShell/LOLBIN hunting en la sección [41](#id-41.-threat-hunting-playbooks-practicos).

#### 46.3 CrowdStrike Falcon

* **Qué es:** EDR (Endpoint Detection & Response) con telemetría de procesos, red y detecciones propias del motor CrowdStrike, ingerida vía el TA/add-on oficial.
* **Campos clave (tras normalización CIM):** `ComputerName`/`dest`, `UserName`/`user`, `FileName`/`process_name`, `CommandLine`/`process`, `ParentBaseFileName`/`parent_process_name`, `SHA256HashData`/`file_hash`, `severity_name` (para detecciones propias del producto).
* **Casos de uso:** correlar detecciones nativas de CrowdStrike con otras fuentes, threat hunting de procesos, respuesta a incidentes.
* **Detección típica:**

```
index=crowdstrike sourcetype=CrowdStrike:Event:Detection earliest=-24h
| stats count values(severity_name) AS severities BY ComputerName UserName
| where count > 5
```

#### 46.4 Microsoft Defender for Endpoint

* **Qué es:** EDR/AV de Microsoft, ingerido vía API (Microsoft Graph Security API) o add-on.
* **Campos clave:** `DeviceName`, `AccountName`, `FileName`, `ProcessCommandLine`, `RemoteIP`, `ActionType` (por ejemplo `ProcessCreated`, `NetworkConnectionEvents`).
* **Casos de uso:** similar a Sysmon/CrowdStrike, con la ventaja de estar ya correlado con Azure AD/Entra ID si se usa Microsoft 365 Defender.
* **Detección típica:** correlación de `ActionType=ProcessCreated` con patrones LOLBIN, igual que en Sysmon.

#### 46.5 Palo Alto Networks (Firewall)

* **Qué es:** logs de tráfico y de amenazas del firewall de nueva generación.
* **Campos clave:** `src_ip`, `dest_ip`, `dest_port`, `app` (aplicación identificada por App-ID), `action` (`allow`/`deny`), `rule`, `threat_name` (log de tipo *threat*).
* **Casos de uso:** detección de tráfico bloqueado sospechoso, comunicación hacia IPs/dominios de baja reputación, aplicaciones no autorizadas.
* **Detección típica:**

```
index=paloalto sourcetype=pan:threat earliest=-24h
| stats count values(threat_name) AS threats BY src_ip dest_ip
| where count > 20
```

#### 46.6 Cisco ASA

* **Qué es:** logs de firewall/VPN de Cisco Adaptive Security Appliance.
* **Campos clave:** `src_ip`, `dest_ip`, `dest_port`, `action`, `message_id` (por ejemplo `%ASA-4-106023` para conexiones denegadas), `user` (en logs de VPN).
* **Casos de uso:** accesos VPN desde ubicaciones atípicas, conexiones denegadas repetidas (posible escaneo).
* **Detección típica:**

```
index=cisco sourcetype=cisco:asa earliest=-24h "Deny"
| stats count BY src_ip dest_ip dest_port
| where count > 50
```

#### 46.7 Bluecoat (Proxy)

* **Qué es:** logs de proxy web (tráfico saliente HTTP/HTTPS de usuarios).
* **Campos clave:** `src_ip` (cliente), `dest_domain`/`url`, `action` (`blocked`/`allowed`), `bytes_out`, `category` (categorización de contenido), `user`.
* **Casos de uso:** exfiltración de datos, acceso a dominios de baja reputación, descargas sospechosas.
* **Detección típica:** ver ejemplo de la sección [4](#id-4.-orden-recomendado-para-optimizar-una-query) (`index=proxy sourcetype=bluecoat action=blocked`).

#### 46.8 Zscaler

* **Qué es:** proxy/SWG (Secure Web Gateway) en la nube; funcionalmente similar a Bluecoat pero SaaS.
* **Campos clave:** `user`, `dept`, `url`, `urlcategory`, `action`, `reqsize`/`respsize` (equivalentes a bytes in/out), `threatname` (si el módulo de threat protection está activo).
* **Casos de uso:** igual que Bluecoat, con la ventaja de cubrir usuarios remotos fuera de la red corporativa.

#### 46.9 Okta

* **Qué es:** proveedor de identidad (IdP) SaaS; logs de autenticación, MFA y aprovisionamiento.
* **Campos clave:** `actor.alternateId` (usuario), `client.ipAddress`, `eventType` (`user.session.start`, `user.authentication.auth_via_mfa`, `user.mfa.factor.deactivate`…), `outcome.result` (`SUCCESS`/`FAILURE`), `debugContext.debugData.threatSuspected`.
* **Casos de uso:** fuerza bruta contra SSO, fatiga de MFA (*MFA bombing*), desactivación sospechosa de factores MFA, *impossible travel*.
* **Detección típica:**

```
index=okta sourcetype=OktaIM2:log earliest=-1h eventType=user.authentication.auth_via_mfa outcome.result=FAILURE
| stats count BY actor.alternateId client.ipAddress
| where count >= 5
```

#### 46.10 Azure AD / Microsoft Entra ID

* **Qué es:** IdP de Microsoft; logs de sign-in y de auditoría (cambios de configuración, roles).
* **Campos clave:** `userPrincipalName`, `ipAddress`, `appDisplayName`, `status.errorCode` (0 = éxito), `location.countryOrRegion`, `conditionalAccessStatus`, `riskLevelDuringSignIn` (si Identity Protection está activo).
* **Casos de uso:** *impossible travel*, bloqueo de acceso condicional evadido, consentimiento OAuth sospechoso (illicit consent grant), fuerza bruta contra Entra ID.
* **Detección típica (impossible travel simplificada):** ver sección [53.2](#id-53.2-impossible-travel).

#### 46.11 AWS CloudTrail

* **Qué es:** log de auditoría de llamadas a la API de AWS (quién hizo qué, desde dónde).
* **Campos clave:** `eventName` (nombre de la API llamada, por ejemplo `ConsoleLogin`, `CreateUser`, `AssumeRole`), `userIdentity.userName`/`userIdentity.arn`, `sourceIPAddress`, `awsRegion`, `errorCode` (presencia = intento fallido/denegado).
* **Casos de uso:** escalada de privilegios en la nube, creación de usuarios/roles no autorizados, exfiltración vía S3, uso anómalo de credenciales.
* **Detección típica:**

```
index=aws sourcetype=aws:cloudtrail earliest=-24h eventName=ConsoleLogin errorCode=*
| stats count BY userIdentity.userName sourceIPAddress
| where count >= 5
```

#### 46.12 Microsoft 365 (Office 365 Management Activity API)

* **Qué es:** logs de actividad de los servicios de Microsoft 365 (Exchange Online, SharePoint, Teams, reglas de buzón).
* **Campos clave:** `Operation` (por ejemplo `New-InboxRule`, `MailItemsAccessed`, `FileDownloaded`), `UserId`, `ClientIP`, `Workload` (`Exchange`, `SharePoint`, `AzureActiveDirectory`).
* **Casos de uso:** reglas de reenvío de correo maliciosas (BEC), descarga masiva de ficheros (posible exfiltración), acceso anómalo a buzones tras un compromiso de cuenta.
* **Detección típica:**

```
index=o365 sourcetype=o365:management:activity Operation="New-InboxRule" earliest=-24h
| where match(lower(Parameters), "forwardto|redirectto")
| table _time UserId Parameters ClientIP
```

***

### 47. CIM Mapping: de campo original a campo CIM

Uno de los conceptos peor entendidos por analistas SOC es *cómo* un campo específico de una fuente concreta llega a convertirse en un campo estándar del CIM. Esta sección hace explícito ese puente, fuente por fuente.

#### 47.1 Tabla de mapeo por fuente

| Fuente             | Campo original              | Campo CIM                                             | Data Model / Dataset             |
| ------------------ | --------------------------- | ----------------------------------------------------- | -------------------------------- |
| Windows Security   | `Account_Name`              | `user`                                                | `Authentication`                 |
| Windows Security   | `Source_Network_Address`    | `src`                                                 | `Authentication`                 |
| Windows Security   | `Logon_Type`                | `authentication_method` (derivado)                    | `Authentication`                 |
| Sysmon             | `Image`                     | `process`                                             | `Endpoint.Processes`             |
| Sysmon             | `CommandLine`               | `process` (o campo `process` completo con argumentos) | `Endpoint.Processes`             |
| Sysmon             | `ParentImage`               | `parent_process`                                      | `Endpoint.Processes`             |
| Sysmon             | `Hashes` (SHA256=…)         | `file_hash`                                           | `Endpoint.Processes` / `Malware` |
| CrowdStrike        | `ComputerName`              | `dest`                                                | `Endpoint.Processes`             |
| CrowdStrike        | `UserName`                  | `user`                                                | `Endpoint.Processes`             |
| CrowdStrike        | `FileName`                  | `process_name`                                        | `Endpoint.Processes`             |
| Bluecoat / Zscaler | `c-ip` / `client.ipAddress` | `src`                                                 | `Web` / `Network_Traffic`        |
| Bluecoat / Zscaler | `cs-host` / `url`           | `url` / `dest`                                        | `Web`                            |
| Palo Alto          | `src`                       | `src`                                                 | `Network_Traffic`                |
| Palo Alto          | `dst`                       | `dest`                                                | `Network_Traffic`                |
| Palo Alto          | `app`                       | `app`                                                 | `Network_Traffic`                |
| Cisco ASA          | `src_ip`                    | `src`                                                 | `Network_Traffic`                |
| Okta               | `actor.alternateId`         | `user`                                                | `Authentication`                 |
| Okta               | `client.ipAddress`          | `src`                                                 | `Authentication`                 |
| Azure AD           | `userPrincipalName`         | `user`                                                | `Authentication`                 |
| Azure AD           | `ipAddress`                 | `src`                                                 | `Authentication`                 |
| AWS CloudTrail     | `userIdentity.userName`     | `user`                                                | `Authentication` / `Change`      |
| AWS CloudTrail     | `sourceIPAddress`           | `src`                                                 | `Authentication`                 |
| Microsoft 365      | `UserId`                    | `user`                                                | `Authentication` / `Web`         |
| Microsoft 365      | `ClientIP`                  | `src`                                                 | `Authentication`                 |

#### 47.2 Por qué esto importa

Sin este mapeo (normalmente implementado vía *field aliases* y *tags* dentro de un TA/add-on de Splunk, o mediante un `props.conf`/`eval` de normalización propio), una búsqueda basada en el data model `Authentication` **no encontrará** eventos de Okta o Azure AD aunque los datos existan en el índice - porque Splunk solo “ve” en el data model lo que ha sido correctamente etiquetado (`tag=authentication`) y mapeado a los campos CIM esperados (`user`, `src`, `action`…).

#### 47.3 Ejemplo de normalización manual (cuando no hay TA/CIM add-on)

Si una fuente no dispone de un add-on CIM-compliant, se puede normalizar manualmente dentro de la propia búsqueda con `eval`/`rename`, a costa de tener que repetir esta lógica en cada detección (razón de más para envolverla en una **macro**, ver sección [17.2](#id-17.2-macros-con-argumentos)):

```
index=okta sourcetype=OktaIM2:log earliest=-24h
| rename actor.alternateId AS user, client.ipAddress AS src
| eval action=if(outcome.result="SUCCESS","success","failure")
| stats count BY user src action
```

***

### 48. Coverage Engineering

El *Coverage Engineering* responde a una pregunta que todo responsable de un programa de detección debe poder contestar en cualquier momento: **¿qué estamos detectando realmente, y qué no?** Sin medir cobertura, un programa de detección crece de forma orgánica y desordenada, con huecos invisibles hasta que un incidente real los expone.

#### 48.1 Las cuatro dimensiones de cobertura

* **Data Source Coverage:** qué fuentes de datos están ingeridas, correctamente parseadas y (idealmente) mapeadas a CIM. Sin esto, ninguna otra dimensión de cobertura tiene sentido: no se puede detectar sobre datos que no existen o no se leen.
* **ATT\&CK Coverage:** qué técnicas (y sub-técnicas) de MITRE ATT\&CK tienen al menos una detección asociada, frente a la matriz completa.
* **Detection Coverage:** cuántas detecciones activas existen por táctica/técnica/fuente de datos, y si esa cobertura es proporcional al riesgo real del entorno (por ejemplo, ¿tenemos 20 detecciones de phishing pero cero de exfiltración vía DNS?).
* **Validation Coverage:** de todas las detecciones existentes, cuántas han sido **probadas** activamente (ver sección [50](#id-50.-testing-de-detecciones)) frente a las que simplemente “se desplegaron y se asume que funcionan”.

#### 48.2 Construir un catálogo de detecciones (base de todo lo demás)

Toda la ingeniería de cobertura depende de tener un **catálogo estructurado** de detecciones, normalmente como lookup o KV Store:

```
detection_name, data_source, mitre_technique_id, mitre_tactic, severity, tested, last_validated, status
```

```
| inputlookup detections_catalog.csv
| table detection_name data_source mitre_technique_id status tested
```

#### 48.3 Medir ATT\&CK Coverage

```
| inputlookup detections_catalog.csv
| where status="active"
| stats dc(detection_name) AS detections BY mitre_technique_id
| append
    [ | inputlookup mitre_attack_techniques.csv
      | fields technique_id technique_name ]
| stats values(detections) AS detections_count values(technique_name) AS technique_name BY mitre_technique_id
| eval covered=if(isnotnull(detections_count), "yes", "no")
```

**Qué hace:** cruza el catálogo de detecciones activas con una lista completa de técnicas ATT\&CK (lookup externo, por ejemplo exportado del ATT\&CK Navigator), marcando qué técnicas están cubiertas y cuáles no. El resultado se puede exportar directamente al formato de capas (*layer*) del [ATT\&CK Navigator](https://mitre-attack.github.io/attack-navigator/) para visualización tipo mapa de calor.

#### 48.4 Medir Data Source Coverage

```
| metadata type=sourcetypes index=*
| eval last_seen_days=round((now()-lastTime)/86400,1)
| where last_seen_days > 2
| table sourcetype last_seen_days
```

**Qué hace:** identifica sourcetypes que llevan más de 2 días sin recibir datos - una fuente “caída” silenciosamente reduce cobertura sin que nadie lo note si no se audita.

#### 48.5 Medir Validation Coverage

```
| inputlookup detections_catalog.csv
| eval days_since_validation=round((now()-strptime(last_validated,"%Y-%m-%d"))/86400,0)
| eval validation_status=case(
    tested="false", "never_tested",
    days_since_validation > 180, "stale",
    true(), "valid"
  )
| stats count BY validation_status
```

**Qué hace:** clasifica cada detección según si nunca ha sido probada, si su última validación es antigua (>180 días, umbral orientativo) o si está vigente, dando una foto rápida de cuán “confiable” es el conjunto completo de detecciones.

#### 48.6 Buenas prácticas

* Revisa la cobertura ATT\&CK **por grupo de amenaza relevante** para tu sector (no toda la matriz tiene el mismo peso para todas las organizaciones).
* Prioriza cerrar huecos de cobertura en técnicas asociadas a los vectores de ataque más probables según tu threat intel, no en orden alfabético de técnicas.
* Revisa Data Source Coverage con la misma frecuencia que Detection Coverage: una detección “activa” sobre una fuente que dejó de fluir da una falsa sensación de seguridad.

***

### 49. Purple Teaming: detección basada en Atomic Red Team

[Atomic Red Team](https://github.com/redcanaryco/atomic-red-team) (mantenido por Red Canary) es una librería open-source de “atomic tests”: pruebas pequeñas y controladas que reproducen técnicas concretas de MITRE ATT\&CK, pensadas para validar si las detecciones existentes realmente disparan ante el comportamiento que dicen cubrir. Es la base práctica de un ejercicio de **Purple Team** (colaboración entre Red Team, que ejecuta, y Blue Team/SOC, que valida detección).

#### 49.1 Ciclo de trabajo

1. **Ejecutar el Atomic Test** correspondiente a la técnica objetivo, en un entorno controlado (máquina de laboratorio, nunca en producción sin autorización explícita).
2. **Generar telemetría:** confirmar que el entorno de laboratorio está enviando logs a Splunk (Sysmon, EDR, etc.) y que el test efectivamente generó eventos.
3. **Validar la detección:** ejecutar la búsqueda SPL de la detección contra la ventana de tiempo exacta del test y confirmar que produce un resultado (verdadero positivo esperado).
4. **Ajustar la detección** si no disparó: puede ser un problema de campos, de umbral, de normalización o de que la fuente de datos no captura ese comportamiento concreto.

#### 49.2 Ejemplo práctico: T1110 (Brute Force)

Ejecución del atomic test (en el host de laboratorio, usando el framework `Invoke-AtomicTest` de PowerShell):

```powershell
Invoke-AtomicTest T1110 -TestNumbers 1
```

Validación en Splunk, acotando la ventana de tiempo exacta de la ejecución del test:

```
index=windows sourcetype=WinEventLog:Security EventCode=4625
earliest="07/22/2026:10:00:00" latest="07/22/2026:10:10:00" host=lab-host-01
| stats count AS failed_logons BY user
```

Si `failed_logons` no alcanza el umbral esperado por la detección real (por ejemplo, el atomic test solo genera 3 intentos y la detección exige `>=10`), esto revela una **brecha de cobertura real**: el atomic test simula un ataque más “silencioso” que el umbral configurado, y conviene añadir una detección complementaria de menor umbral con mayor contexto (por ejemplo, correlacionada con IP externa) para no perder ese escenario.

#### 49.3 Ejemplo práctico: T1218 (LOLBIN - `regsvr32`)

```powershell
Invoke-AtomicTest T1218.010 -TestNumbers 1
```

```
index=windows sourcetype=Sysmon earliest="07/22/2026:11:00:00" latest="07/22/2026:11:10:00" host=lab-host-01
| where match(lower(process_name), "regsvr32")
| table _time host user process_name command_line parent_process
```

#### 49.4 Integración con el catálogo de detecciones

Cada vez que se ejecuta un ciclo de Atomic Red Team contra una detección, el resultado (disparó / no disparó / disparó parcialmente) debería registrarse en el catálogo de detecciones (sección [48.2](#id-48.2-construir-un-catalogo-de-detecciones-base-de-todo-lo-demas)), actualizando los campos `tested` y `last_validated`. Esto conecta directamente Purple Teaming con Coverage Engineering: sin este registro, ejecutar atomics es un ejercicio puntual sin memoria organizativa.

***

### 50. Testing de detecciones

Validar una detección antes (y periódicamente después) de ponerla en producción reduce tanto falsos negativos (no detecta lo que debería) como falsos positivos (satura al analista). Esta sección complementa la metodología de la sección [39](#id-39.-metodologia-de-detection-engineering) con el “cómo probar” en sí mismo.

#### 50.1 Indicadores positivos (verdaderos positivos esperados)

Un conjunto de escenarios que **deben** disparar la detección. Se puede generar mediante Atomic Red Team (sección [49](#id-49.-purple-teaming-deteccion-basada-en-atomic-red-team)) o mediante datos sintéticos (sección [50.3](#id-50.3-datos-sinteticos-con-makeresults)). Cada indicador positivo debe documentarse junto al catálogo de detecciones, para poder re-ejecutarlo como test de regresión cuando la detección cambie.

#### 50.2 Indicadores negativos (verdaderos negativos / anti-falsos-positivos)

Un conjunto de escenarios **legítimos y conocidos** que **no deben** disparar la detección: por ejemplo, el patrón habitual de reintentos de una cuenta de servicio, o un script de administración autorizado que usa PowerShell con parámetros similares a los de un atacante. Ejecutar la detección contra estos escenarios tras cada cambio evita reintroducir falsos positivos ya solucionados anteriormente (regresión).

#### 50.3 Datos sintéticos con `makeresults`

Cuando no se puede (o no conviene) generar el comportamiento real en un laboratorio, se puede simular directamente el evento esperado con `makeresults`, para validar exclusivamente la **lógica SPL** de la detección (no la capacidad de captura de la fuente de datos):

```
| makeresults count=12
| streamstats count AS i
| eval _time=now()-(i*30)
| eval user="test.user"
| eval EventCode=4625
| eval src_ip="203.0.113.".i
| fields _time user EventCode src_ip
```

Este bloque genera 12 eventos sintéticos de login fallido, espaciados 30 segundos entre sí, con IPs distintas. Se puede concatenar con `append` a una búsqueda real (en un índice de pruebas) para verificar que la lógica de `stats`/`where` de la detección produce el resultado esperado, sin depender de generar tráfico real.

#### 50.4 Atomic Red Team y Red Canary

Ver sección [49](#id-49.-purple-teaming-deteccion-basada-en-atomic-red-team). Además de la librería Atomic Red Team, Red Canary publica periódicamente el informe *Threat Detection Report*, útil como fuente de indicadores positivos priorizados por prevalencia real observada en el mundo, en vez de cubrir la matriz ATT\&CK de forma uniforme.

#### 50.5 Replay de logs históricos

Cuando se dispone de logs de un incidente real pasado (propio o de threat intel compartida), una técnica de validación muy potente es “reproducirlos” contra la detección actual, ajustando los timestamps para que caigan dentro de la ventana de búsqueda de la detección:

```
| inputlookup incident_2025_logs.csv
| eval _time=now()-3600+relative_offset
| collect index=replay_test sourcetype=WinEventLog:Security
```

Después, se ejecuta la detección apuntando a `index=replay_test` en vez del índice real, para confirmar si la lógica actual **habría** detectado ese incidente conocido. Esta es una de las formas más fiables de auditar retroactivamente la cobertura real de una detección frente a comportamiento de ataque genuino (no simulado).

#### 50.6 Frecuencia de testing recomendada

| Tipo de cambio                        | Testing recomendado                                                                                                      |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Detección nueva                       | Indicador positivo (Atomic o sintético) + indicador negativo, antes de pasar a producción.                               |
| Cambio de umbral                      | Regresión completa (positivos + negativos conocidos).                                                                    |
| Cambio de fuente de datos / add-on    | Verificar Data Source Coverage (sección [48.4](#id-48.4-medir-data-source-coverage)) + reejecutar indicadores positivos. |
| Sin cambios (mantenimiento periódico) | Ciclo de Atomic Red Team trimestral/semestral por técnica crítica.                                                       |

***

### 51. Production Readiness Checklist

Antes de desplegar una detección en producción (es decir, que empiece a generar notables/incidentes reales que un analista deberá triar), se recomienda pasar por esta checklist:

* [ ] **CIM validado**: los campos usados por la detección están correctamente mapeados (o normalizados manualmente) para todas las fuentes relevantes.
* [ ] **ATT\&CK asignado**: la detección tiene un `mitre_technique_id` documentado (sección [44](#id-44.-mapeo-att-and-ck-para-detecciones-spl)).
* [ ] **Severity definida**: criterio explícito de qué hace que un resultado sea `low`/`medium`/`high`/`critical`, no un valor fijo arbitrario.
* [ ] **RBA definida**: si aplica el modelo de riesgo (sección [40](#id-40.-risk-based-alerting-rba)), el `risk_score` y el `risk_object`/`risk_object_type` están definidos y documentados.
* [ ] **Falsos positivos documentados**: se han identificado y, cuando es posible, excluido explícitamente (lookup de exclusiones, condiciones adicionales) los patrones legítimos conocidos (sección [45](#id-45.-guia-de-reduccion-de-falsos-positivos)).
* [ ] **Tuning realizado**: el umbral se ha calibrado con datos reales (baseline), no con un número supuesto.
* [ ] **Rendimiento validado**: la búsqueda se ejecuta en un tiempo razonable con el `earliest`/`latest` real de producción (ver siguiente punto).
* [ ] **Job Inspector revisado**: `scanCount` razonable frente a `eventCount`, sin comandos inesperadamente costosos (sección [37](#id-37.-search-job-inspector-como-identificar-busquedas-lentas)).
* [ ] **Drilldown creado**: el analista que reciba el notable tiene una forma clara de “profundizar” (drilldown) hacia los eventos originales, no solo un número agregado.
* [ ] **Caso investigable**: la salida final incluye entidad, evidencia (IPs, hosts, comandos) y contexto temporal suficiente para investigar sin tener que reconstruir manualmente la búsqueda desde cero.

#### Plantilla de sign-off

```
Detección:          Multiple Failed Logons Followed By Success
CIM validado:       Sí (Authentication data model, fuentes: Windows, Okta)
ATT&CK:             T1110 (Brute Force)
Severity:           High
RBA:                risk_score=40, risk_object=user
FPs documentados:   Sí (cuentas de servicio excluidas vía lookup)
Tuning:             Umbral calibrado sobre baseline de 14 días
Rendimiento:        scanCount/eventCount ratio OK, runDuration < 5s
Job Inspector:      Revisado 2026-07-20
Drilldown:          Sí, hacia _raw filtrado por user y ventana de tiempo
Aprobado por:       <nombre> - <fecha>
```

***

### 52. SPL vs SPL2

SPL (el lenguaje descrito en todo este documento) sigue siendo, a día de hoy, el lenguaje dominante y con el que se escribe la inmensa mayoría de detecciones, dashboards y reportes en Splunk Enterprise y Splunk Enterprise Security. **SPL2** es una evolución del lenguaje de búsqueda de Splunk, pensada para un modelo más explícito de “datasets” y para dar soporte a capacidades más recientes de la plataforma (por ejemplo, procesamiento de datos en el momento de la ingesta y búsqueda federada entre múltiples fuentes/entornos). Su disponibilidad y alcance concretos dependen de la versión y del producto Splunk (Splunk Cloud vs Enterprise, y del componente específico), por lo que conviene verificar siempre la documentación oficial vigente para el entorno concreto antes de adoptarlo en producción.

#### 52.1 Diferencias conceptuales principales

* **Orientación a datasets explícitos:** en vez de empezar con `index=... sourcetype=...` de forma implícita, SPL2 tiende a usar una sintaxis más explícita del tipo `from <dataset>` para declarar sobre qué conjunto de datos se opera, acercándose conceptualmente a un lenguaje de tipo SQL/dataframe.
* **`from` y `dataset`:** en SPL2, el punto de partida de una consulta suele ser una referencia explícita a un dataset (que puede ser un índice, una vista o una fuente federada), en lugar de asumir siempre un índice de Splunk clásico.
* **Sintaxis de pipeline más uniforme:** SPL2 busca una gramática más consistente entre comandos (por ejemplo, nombres de argumentos más predecibles), pensada también para facilitar la generación/validación de queries por herramientas externas.
* **Pensado para nuevos componentes de la plataforma:** SPL2 se usa en piezas más recientes del ecosistema Splunk orientadas a procesamiento de datos en tránsito y búsqueda federada entre múltiples orígenes, un espacio en evolución activa por parte del fabricante.

#### 52.2 Qué significa esto para este documento

* **SPL clásico no está obsoleto**: sigue siendo el lenguaje soportado y recomendado para Splunk Enterprise Security, la inmensa mayoría de detecciones existentes, y el que se documenta en profundidad en todo este manual.
* **Vale la pena estar al tanto de SPL2** si tu organización empieza a adoptar componentes más nuevos de la plataforma que lo requieran, pero no es necesario reescribir detecciones SPL existentes sin una razón concreta.
* Antes de invertir tiempo migrando lógica a SPL2, confirma con la documentación oficial de Splunk vigente en el momento si el componente que usas (Enterprise Security, dashboards clásicos, alertas programadas) realmente lo requiere o si sigue funcionando exclusivamente sobre SPL clásico.

***

### 53. Anexo: Enterprise Security Detections Library

Biblioteca de detecciones completas, siguiendo el framework de la sección [39](#id-39.-metodologia-de-detection-engineering) (Hipótesis / MITRE / SPL / Tuning / FPs / RBA), para los escenarios de ataque más solicitados en un SOC maduro.

#### 53.1 Password Spray

* **Hipótesis:** un atacante prueba **una misma contraseña** (o un set reducido) contra **muchos usuarios distintos**, en vez de muchas contraseñas contra un mismo usuario (lo que evita bloqueos por umbral de intentos por cuenta).
* **MITRE:** T1110.003 (Brute Force: Password Spraying).
* **SPL:**

```
index=windows sourcetype=WinEventLog:Security EventCode=4625 earliest=-15m@m latest=-5m@m
| stats dc(user) AS distinct_users count AS total_failures BY src_ip
| where distinct_users >= 15 AND total_failures >= 20
```

* **Tuning:** ajustar `distinct_users`/`total_failures` según el tamaño real del entorno (un entorno de 50.000 usuarios tolera umbrales más altos que uno de 500).
* **FPs:** escáneres de vulnerabilidades autorizados, migraciones masivas de contraseñas, sincronización de identidad mal configurada.
* **RBA:**

```
| eval risk_object=src_ip, risk_object_type="system", risk_score=50, mitre_technique_id="T1110.003"
```

* **Entidad esperada:** `src_ip` (origen del ataque) como `risk_object`; opcionalmente `distinct_users` como lista de posibles cuentas afectadas para la investigación.
* **Volumen esperado:** en un entorno de tamaño medio, 0-2 disparos/día; picos de más de 5/día en ventanas de 15 minutos son indicativos de campaña activa.
* **Pasos de investigación:** (1) confirmar si `src_ip` es una IP externa o interna conocida; (2) revisar si alguno de los `distinct_users` tuvo un login exitoso (4624) tras los fallos - sería la señal más crítica de spray exitoso; (3) comprobar reputación de la IP en threat intel; (4) si es exitoso, iniciar respuesta de reseteo de credenciales para las cuentas afectadas.

#### 53.2 Impossible Travel

* **Hipótesis:** un mismo usuario inicia sesión desde dos ubicaciones geográficas cuya distancia es incompatible con el tiempo transcurrido entre ambos logins (viaje “físicamente imposible”), indicando credenciales comprometidas usadas desde dos sitios distintos.
* **MITRE:** T1078 (Valid Accounts).
* **SPL:**

```
index=azuread sourcetype=azure:aad:signin earliest=-24h status.errorCode=0
| iplocation ipAddress
| sort 0 userPrincipalName _time
| streamstats current=f last(lat) AS prev_lat last(lon) AS prev_lon last(_time) AS prev_time BY userPrincipalName
| eval distance_km=round(haversine(lat,lon,prev_lat,prev_lon),0)
| eval hours_elapsed=round((_time-prev_time)/3600,2)
| eval implied_speed_kmh=round(distance_km/hours_elapsed,0)
| where hours_elapsed > 0 AND implied_speed_kmh > 900
| table _time userPrincipalName distance_km hours_elapsed implied_speed_kmh
```

> **ADVERTENCIA - `haversine()` NO es una función SPL estándar.** No existe out-of-the-box en el catálogo de funciones `eval` de Splunk (ver sección [12](#id-12.-funciones-de-evaluacion-eval-functions)). Si copias este query tal cual, **fallará** salvo que tu entorno tenga registrada una función de búsqueda personalizada (custom search command) con ese nombre. Alternativas reales para producción:

1. Implementar la fórmula del gran círculo manualmente con funciones `eval` nativas (`acos`, `sin`, `cos`, `pi()`), por ejemplo: `distance_km=6371*acos(sin(lat1*pi()/180)*sin(lat2*pi()/180)+cos(lat1*pi()/180)*cos(lat2*pi()/180)*cos((lon2-lon1)*pi()/180))`.
2. Usar un add-on que calcule distancia como función de búsqueda personalizada instalada explícitamente.
3. **Recomendado si la fuente es Azure AD**: usar directamente el campo `riskLevelDuringSignIn` del add-on de Identity Protection, que ya resuelve este riesgo de forma nativa sin necesidad de cálculo geográfico manual.

* **Tuning:** excluir rangos de IP de VPN corporativa conocidos (que pueden simular saltos de ubicación falsos).
* **FPs:** VPNs corporativas, proveedores de conectividad móvil con salida NAT variable, viajes reales en avión con VPN de a bordo.
* **RBA:**

>

```
| eval risk_object=userPrincipalName, risk_object_type="user", risk_score=60, mitre_technique_id="T1078"
```

* **Entidad esperada:** `userPrincipalName` (usuario) como `risk_object`.
* **Volumen esperado:** muy bajo en un entorno maduro con exclusiones de VPN bien mantenidas (idealmente 0-1/semana); un aumento súbito sugiere revisar si se ha desplegado una nueva VPN/proxy no incluida en las exclusiones.
* **Pasos de investigación:** (1) contactar al usuario por un canal alternativo (no email, podría estar comprometido) para confirmar si viajó o usó VPN; (2) revisar el historial de `deviceDetail.deviceId` - un dispositivo nuevo no reconocido aumenta la sospecha; (3) revisar actividad posterior al segundo login (descargas masivas, reglas de reenvío de correo, cambios de MFA); (4) si no se confirma viaje legítimo, forzar cierre de sesión y rotación de credenciales.

#### 53.3 Suspicious PowerShell

Ver desarrollo completo en la sección [20.3](#id-20.3-powershell-sospechoso) y [41.2](#id-41.2-powershell-hunting). Resumen MITRE: **T1059.001** (Command and Scripting Interpreter: PowerShell). RBA sugerido: `risk_score=30` (señal de confianza media, pensada para combinarse con otras señales vía RBA en vez de generar notable directo).

#### 53.4 LOLBIN Execution

Ver desarrollo completo en la sección [41.3](#id-41.3-lolbin-hunting-living-off-the-land-binaries). MITRE: **T1218** (System Binary Proxy Execution), con sub-técnicas específicas por binario (por ejemplo T1218.010 para `regsvr32`, T1218.005 para `mshta`).

#### 53.5 Token Theft

* **Hipótesis:** un atacante roba un token de sesión/autenticación válido (por ejemplo, cookie de sesión o token OAuth) y lo reutiliza desde una máquina/ubicación distinta a la del usuario legítimo, evitando así tener que volver a autenticarse.
* **MITRE:** T1550.001 (Use Alternate Authentication Material: Application Access Token) / T1539 (Steal Web Session Cookie).
* **SPL (indicio: mismo token/sesión usado desde IPs o user-agents muy distintos en poco tiempo):**

```
index=azuread sourcetype=azure:aad:signin earliest=-1h status.errorCode=0
| stats dc(ipAddress) AS distinct_ips dc(deviceDetail.deviceId) AS distinct_devices values(ipAddress) AS ips BY userPrincipalName sessionId
| where distinct_ips > 1 OR distinct_devices > 1
```

* **Tuning:** requiere que la fuente exponga un identificador de sesión/token estable (`sessionId` o equivalente); sin ello, esta detección no es viable con fiabilidad.
* **FPs:** balanceo de tráfico corporativo con IPs de salida variables, aplicaciones que rotan de dispositivo legítimamente (por ejemplo, continuidad de sesión entre escritorio y móvil en algunas suites ofimáticas).
* **RBA:** `risk_score=70` (señal de alta confianza si el dato de sesión es fiable).
* **Entidad esperada:** `userPrincipalName` como `risk_object`; `sessionId` como campo de correlación para pivotar a otras detecciones.
* **Volumen esperado:** debería ser prácticamente 0 en operación normal; cualquier disparo merece revisión manual inmediata (no es una detección de alto volumen/baja severidad).
* **Pasos de investigación:** (1) identificar todas las IPs/dispositivos asociados al `sessionId`; (2) revisar si el token se usó para acciones sensibles (descarga de datos, cambios de configuración, creación de reglas de buzón); (3) revocar el token/sesión de forma inmediata (`Revoke-AzureADUserAllRefreshToken` o equivalente); (4) forzar re-autenticación con MFA.

#### 53.6 Privilege Escalation

* **Hipótesis:** una cuenta no privilegiada obtiene o se añade a un grupo/rol privilegiado fuera de un proceso de gestión de accesos habitual.
* **MITRE:** T1078.003 (Valid Accounts: Local Accounts) / T1098 (Account Manipulation).
* **SPL:**

```
index=windows sourcetype=WinEventLog:Security EventCode IN (4728,4732,4756) earliest=-1h
| eval group_type=case(EventCode=4728,"Global",EventCode=4732,"Local",EventCode=4756,"Universal")
| table _time Target_User_Name Group_Name group_type Subject_User_Name
```

* **Tuning:** cruzar con un lookup de “administradores/gestores de acceso autorizados” (`Subject_User_Name`) para reducir ruido de altas legítimas realizadas por el equipo de IAM.
* **FPs:** procesos de onboarding/incorporación de empleados, rotación de permisos programada.
* **RBA:** `risk_score=50`, mayor si `Group_Name` corresponde a un grupo crítico (Domain Admins, Enterprise Admins).
* **Entidad esperada:** `Target_User_Name` (cuenta que recibe el privilegio) como `risk_object`; `Subject_User_Name` (quien realizó el cambio) como campo de contexto.
* **Volumen esperado:** bajo y predecible - la mayoría de organizaciones realizan cambios de grupo solo durante ventanas de onboarding/gestión de accesos; fuera de esas ventanas, el volumen esperado es cercano a 0.
* **Pasos de investigación:** (1) verificar si `Subject_User_Name` es un administrador de IAM autorizado y si existe un ticket de cambio asociado; (2) comprobar si `Target_User_Name` ya tenía necesidad de negocio para el grupo; (3) revisar actividad de `Target_User_Name` inmediatamente después de la escalada (acceso a recursos que antes no tenía); (4) si no hay ticket de cambio, revertir la membresía y escalar como incidente.

#### 53.7 Kerberoasting

* **Hipótesis:** un atacante solicita tickets de servicio Kerberos (TGS) para cuentas de servicio con SPN (Service Principal Name) configurado, con el objetivo de extraer el hash del ticket y crackearlo offline para obtener la contraseña de la cuenta de servicio.
* **MITRE:** T1558.003 (Steal or Forge Kerberos Tickets: Kerberoasting).
* **SPL (indicio: volumen anómalo de peticiones 4769 con tipo de cifrado débil, RC4):**

```
index=windows sourcetype=WinEventLog:Security EventCode=4769 earliest=-1h
| where Ticket_Encryption_Type="0x17"
| stats count dc(Service_Name) AS distinct_spns BY Account_Name
| where distinct_spns >= 5
```

* **Tuning:** el cifrado `0x17` (RC4) es sospechoso porque los entornos modernos deberían usar AES; sin embargo, algunas cuentas de servicio legítimas antiguas aún lo usan - cruzar con inventario de cuentas de servicio conocidas.
* **FPs:** cuentas de servicio heredadas configuradas solo con RC4 por compatibilidad con sistemas legacy.
* **RBA:** `risk_score=60`, `risk_object` = `Account_Name`.

> Aclaración sobre entidades en el evento 4769: `Account_Name` es la cuenta que **solicita** el ticket (potencialmente el atacante o una cuenta comprometida usada para el ataque), `Service_Name` es el SPN/cuenta de servicio **objetivo** cuyo hash se intenta crackear, y `Client_Address` es el host desde el que se origina la solicitud. Si el objetivo es puntuar al solicitante (detectar quién está ejecutando el ataque), usa `risk_object=Account_Name`, `risk_object_type="user"`. Si además se quiere dar seguimiento a qué cuentas de servicio están siendo atacadas (para priorizar su rotación de contraseña), añade `Service_Name` como campo de contexto/entidad secundaria - no lo llames “cuenta objetivo” bajo `Account_Name`, ya que puede llevar a confusión sobre quién es el atacante y quién el afectado.

* **Entidad esperada:** `Account_Name` (solicitante del ticket) como `risk_object` principal; `Service_Name` (cuenta de servicio potencialmente atacada) como campo de contexto/entidad secundaria; el host solicitante (`Client_Address`) como campo de contexto adicional para identificar el origen del atacante.
* **Volumen esperado:** en un dominio con pocas cuentas de servicio con SPN, 0 disparos/día es lo esperado; cualquier pico de solicitudes TGS RC4 hacia múltiples SPNs desde un mismo host es altamente sospechoso.
* **Pasos de investigación:** (1) identificar el host origen (`Client_Address`) y el usuario que solicitó los tickets (`Account_Name`); (2) comprobar si ese usuario tiene motivo legítimo para acceder a los servicios listados en `Service_Name`; (3) revisar si posteriormente se observan intentos de autenticación con alguna de las cuentas de servicio objetivo (`Service_Name`) desde un host distinto (indicaría crackeo exitoso); (4) forzar rotación de contraseña de las cuentas de servicio afectadas (`Service_Name`) y migrar a AES si es posible.

#### 53.8 DCShadow

* **Hipótesis:** un atacante con privilegios elevados registra temporalmente un sistema no autorizado como controlador de dominio para replicar cambios maliciosos (por ejemplo, un backdoor de permisos) sin que aparezcan en los logs normales de auditoría de AD.
* **MITRE:** T1207 (Rogue Domain Controller).
* **SPL (indicio: eventos de replicación de directorio originados por un host que no está en la lista conocida de DCs):**

```
index=windows sourcetype=WinEventLog:Security (EventCode=4662 OR EventCode=5136) earliest=-1h
| lookup known_domain_controllers.csv host OUTPUT is_known_dc
| where isnull(is_known_dc)
| table _time host Account_Name Object_Name
```

> **Validar antes de producción:** `EventCode 5136` (Directory Service Changes) depende de que la **auditoría avanzada de Active Directory** (Advanced Audit Policy → “Audit Directory Service Changes”) esté habilitada y correctamente configurada; no todos los entornos la tienen activa por defecto. Igualmente, confirma el `sourcetype`/`source` real usado en tu entorno para estos eventos (puede variar según el Technology Add-on de Windows instalado) antes de copiar esta detección tal cual - no asumas que siempre será `WinEventLog:Security` sin verificarlo.

* **Tuning:** mantener actualizado el lookup `known_domain_controllers.csv`; cualquier DC nuevo legítimo debe añadirse de inmediato tras su aprovisionamiento oficial.
* **FPs:** aprovisionamiento legítimo de un nuevo controlador de dominio no reflejado a tiempo en el lookup.
* **RBA:** `risk_score=90` (alta severidad; DCShadow es una técnica avanzada con bajo volumen de falsos positivos si el lookup está actualizado).
* **Entidad esperada:** `host` (el sistema no autorizado que se registra como DC) como `risk_object`; `Account_Name` como campo de contexto (requiere privilegios de Domain Admin o similar).
* **Volumen esperado:** 0 en operación normal fuera de eventos de aprovisionamiento planificado; esta detección debe tratarse siempre como incidente de severidad crítica, no como señal RBA de bajo volumen.
* **Pasos de investigación:** (1) confirmar de inmediato si el host es un DC legítimo recién aprovisionado (contactar al equipo de infraestructura); (2) si no lo es, aislar el host de la red inmediatamente; (3) revisar qué objetos de AD se replicaron/modificaron alrededor de ese evento; (4) iniciar procedimiento de respuesta a incidentes de compromiso de dominio (asumir compromiso total de AD hasta demostrar lo contrario).

#### 53.9 Golden Ticket

* **Hipótesis:** un atacante que ha comprometido el hash de la cuenta `krbtgt` puede forjar tickets Kerberos (TGT) válidos para cualquier usuario, incluyendo cuentas inexistentes, sin pasar por el controlador de dominio real para la autenticación inicial.
* **MITRE:** T1558.001 (Steal or Forge Kerberos Tickets: Golden Ticket).
* **SPL (indicios clásicos: TGTs con tiempo de vida anómalo, o autenticación de una cuenta ya deshabilitada/inexistente):**

```
index=windows sourcetype=WinEventLog:Security EventCode=4768 earliest=-1h
| lookup identity.csv user AS Account_Name OUTPUT account_status
| where account_status="disabled" OR isnull(account_status)
| table _time Account_Name Client_Address Ticket_Options
```

> Nota de sintaxis del `lookup`: `identity.csv user AS Account_Name` significa “toma el campo `user` de `identity.csv` como clave de match, y compáralo contra el campo `Account_Name` del evento” - es decir, el campo del lookup (`user`) se declara primero y `AS Account_Name` indica el campo del evento con el que debe emparejarse. Esta forma dejar claro que `user` es el nombre de campo en el CSV de referencia, mientras que `Account_Name` es el nombre de campo en el evento de Windows.

* **Tuning:** requiere un lookup de identidad actualizado (`identity.csv`) que refleje fielmente qué cuentas están activas/deshabilitadas/eliminadas.
* **FPs:** desincronización entre el lookup de identidad y el estado real en AD (lookup desactualizado).
* **RBA:** `risk_score=95` (uno de los indicadores de compromiso de dominio más críticos).
* **Entidad esperada:** `Account_Name` (la cuenta forjada) como `risk_object`; `Client_Address` como campo de contexto para localizar el host atacante.
* **Volumen esperado:** 0 en operación normal; como con DCShadow, cualquier disparo debe tratarse como incidente crítico y no como señal de baja severidad para RBA.
* **Pasos de investigación:** (1) verificar en AD si la cuenta existe realmente y su estado (activa/deshabilitada/eliminada); (2) revisar el `Ticket_Options` y el tiempo de vida del ticket en busca de valores anómalos; (3) identificar todos los recursos accedidos con ese ticket forjado; (4) asumir compromiso del hash `krbtgt` y planificar doble reseteo de la cuenta `krbtgt` (procedimiento estándar de recuperación tras Golden Ticket).

#### 53.10 DNS Exfiltration

* **Hipótesis:** un atacante codifica datos dentro de subdominios de consultas DNS hacia un dominio bajo su control, usando el protocolo DNS como canal encubierto de exfiltración (a menudo permitido por firewalls que no inspeccionan DNS en profundidad).
* **MITRE:** T1048.003 (Exfiltration Over Alternative Protocol: DNS) / T1071.004 (Application Layer Protocol: DNS, si se usa también como C2).
* **SPL:**

```
index=dns earliest=-1h
| eval query_length=len(query)
| eval label_count=mvcount(split(query,"."))
| eval avg_label_length=round(query_length/label_count,1)
| eval root_domain=mvjoin(mvindex(split(query,"."),-2,-1),".")
| stats count avg(query_length) AS avg_len values(query) AS sample_queries BY src_ip root_domain
| where avg_len > 50
```

> Nota: calcular `root_domain` en un `eval` explícito (en vez de agrupar directamente por la expresión `mvindex(split(query,"."),-2,-1)` dentro de `BY`) es más legible, más fácil de depurar (`| table src_ip root_domain avg_len`) y evita evaluar la misma expresión dos veces si se reutiliza en pasos posteriores del pipeline.

* **Tuning:** excluir dominios de CDNs y servicios legítimos con subdominios largos por diseño (por ejemplo, ciertos proveedores cloud); ajustar el umbral de longitud media según el entorno.
* **FPs:** CDNs, balanceadores de carga con DNS-based routing, algunas soluciones de seguridad basadas en DNS que legítimamente generan subdominios largos.
* **RBA:** `risk_score=55`, `risk_object` = `src_ip`.
* **Entidad esperada:** `src_ip` (host interno que genera las consultas) como `risk_object`.
* **Volumen esperado:** depende mucho del entorno; establecer baseline por segmento de red durante al menos 7 días antes de fijar el umbral de `avg_len`, ya que algunas aplicaciones legítimas (antivirus, EDR con telemetría por DNS) generan subdominios largos de forma rutinaria.
* **Pasos de investigación:** (1) revisar el dominio de destino (`mvindex(split(query,"."),-2,-1)`) contra threat intel y WHOIS (dominios recién registrados son más sospechosos); (2) calcular el volumen total de datos exfiltrados estimando bytes por consulta × número de consultas; (3) identificar el proceso/host origen mediante EDR (Sysmon Event ID 22 - DNS query, sección [46.2](#id-46.2-sysmon-microsoft-windows-sysmon-operational)); (4) bloquear el dominio en el firewall/DNS resolver y aislar el host si se confirma exfiltración activa.

#### 53.11 Formato reutilizable para nuevas entradas de la librería

```
Nombre:              <nombre de la detección>
Hipótesis:           <qué comportamiento se sospecha y por qué es relevante>
MITRE:               <technique_id> (<nombre de la técnica>)
SPL:                 <query completa, siguiendo el orden de optimización de la sección 4>
Tuning:              <cómo calibrar el umbral con datos reales del entorno>
FPs:                 <patrones legítimos conocidos que pueden disparar falsos positivos>
RBA:                 risk_object=<entidad>, risk_object_type=<tipo>, risk_score=<n>
Entidad esperada:    <qué campo es risk_object y por qué es la entidad correcta a puntuar>
Volumen esperado:    <cuántos disparos/día son normales en un entorno de referencia, y qué desviación es señal de alerta>
Pasos de investigación: <lista ordenada de acciones concretas que debe seguir un analista L1/L2 al recibir esta alerta>
```

***

### 54. Data Onboarding y Parsing Pipeline

Todo lo anterior en este documento asume que los datos **ya están en Splunk, con los campos correctos**. Esta sección explica cómo llegan realmente esos datos: el recorrido desde el origen hasta que un evento es buscable con `index=... sourcetype=...`.

#### 54.1 Componentes de la ingesta

<table><thead><tr><th width="236">Componente</th><th width="470">Rol</th></tr></thead><tbody><tr><td><strong>Universal Forwarder (UF)</strong></td><td>Agente ligero instalado en el origen (servidor, endpoint). Reenvía datos en crudo, sin parsear (parsing mínimo). No consume apenas CPU/memoria.</td></tr><tr><td><strong>Heavy Forwarder (HF)</strong></td><td>Forwarder con el pipeline de parsing completo activado. Puede parsear, enriquecer, filtrar y enrutar eventos <strong>antes</strong> de que lleguen al indexer. Se usa cuando se necesita transformar datos en el origen (por ejemplo, filtrar ruido, enmascarar PII, o soportar entradas modulares como syslog/HEC de terceros).</td></tr><tr><td><strong>Indexer</strong></td><td>Recibe los datos (ya parseados por el HF, o en crudo desde un UF) y ejecuta el resto del pipeline de indexación: los escribe en <code>.tsidx</code> y <code>rawdata</code> dentro de los <em>buckets</em>.</td></tr><tr><td><strong>Search Head</strong></td><td>No participa en la ingesta; solo distribuye búsquedas a los indexers y agrega resultados.</td></tr></tbody></table>

> Regla práctica: usa **UF** siempre que sea posible (menor huella, más simple de mantener a escala). Usa **HF** solo cuando necesites lógica de parsing/enrutamiento antes del indexer (cumplimiento normativo, filtrado de volumen, integración con TAs que requieren ejecución en el forwarder).

#### 54.2 El pipeline de indexación: las 4 colas

Cuando un evento llega a un indexer (o a un HF con parsing activado), pasa secuencialmente por varias colas internas:

```
Input Pipeline
     ↓
Parsing Queue   → aquí se aplica LINE_BREAKER, se determina el timestamp (TIME_PREFIX/TIME_FORMAT),
                  se anota metadata (host/source/sourcetype), se aplican transforms.conf (SEDCMD, enmascarado)
     ↓
Merging Queue   → reensambla eventos multilínea si es necesario
     ↓
Typing Queue    → aplica reglas de props.conf/transforms.conf que dependen de haber "tipado" el evento
                  (por ejemplo, asignación final de sourcetype vía transforms basados en contenido)
     ↓
Index Queue     → escribe el evento final en los buckets del índice (.tsidx + rawdata)
```

* **Parsing Queue:** es donde ocurre el “cortado” de eventos individuales a partir del flujo de datos crudo, y donde se decide el timestamp de cada evento.
* **Typing Queue:** es donde se resuelve la clasificación final del evento (sourcetype, aplicación de `TRANSFORMS-*` que reescriben metadata según el contenido).
* **Index Queue:** el paso final antes de que el evento sea persistido y buscable.

Cuando una búsqueda va lenta por “indexación”, el problema casi siempre está en una de estas colas saturada (visible en `_internal` vía `index=_internal source=*metrics.log group=queue`).

#### 54.3 `props.conf`: cómo Splunk interpreta un evento

`props.conf` define, por `sourcetype` (o por `source`/`host`), cómo se debe **interpretar** el dato crudo antes de indexarlo. Las directivas más importantes:

```
[mi_sourcetype]
LINE_BREAKER= ([\r\n]+)
SHOULD_LINEMERGE=false
TIME_PREFIX= ^
TIME_FORMAT= %Y-%m-%d %H:%M:%S
MAX_TIMESTAMP_LOOKAHEAD=25
KV_MODE= json
INDEXED_EXTRACTIONS= json
TRUNCATE=10000
```

<table data-search="false"><thead><tr><th>Directiva</th><th>Qué hace</th><th>Cuándo importa</th></tr></thead><tbody><tr><td><code>LINE_BREAKER</code></td><td>Regex que determina dónde termina un evento y empieza el siguiente. Por defecto, salto de línea; en logs multilínea (stack traces, JSON pretty-printed) hay que definirlo explícitamente.</td><td>Logs multilínea mal cortados generan eventos fragmentados o gigantes - causa nº1 de “por qué mi log no se ve bien en Splunk”.</td></tr><tr><td><code>SHOULD_LINEMERGE</code></td><td>Si <code>true</code>, Splunk intenta juntar líneas que “parecen” del mismo evento (heurística costosa). Se recomienda <code>false</code> + <code>LINE_BREAKER</code> explícito para mayor control y rendimiento.</td><td>Rendimiento de parsing a escala.</td></tr><tr><td><code>TIME_PREFIX</code></td><td>Regex que indica dónde, dentro del evento, empieza el timestamp.</td><td>Si el timestamp no está al principio del evento (por ejemplo, tras un nivel de log <code>[INFO]</code>), Splunk puede extraer mal el <code>_time</code> sin esto.</td></tr><tr><td><code>TIME_FORMAT</code></td><td>Formato <code>strptime</code> del timestamp (ver función <code>strptime()</code>, sección <a href="#id-12.-funciones-de-evaluacion-eval-functions">12</a>).</td><td>Timestamps ambiguos (<code>01/02/2026</code>, ¿día o mes primero?) o con zona horaria no estándar.</td></tr><tr><td><code>MAX_TIMESTAMP_LOOKAHEAD</code></td><td>Cuántos caracteres desde <code>TIME_PREFIX</code> se escanean para encontrar el timestamp.</td><td>Timestamps muy largos o con prefijos variables.</td></tr><tr><td><code>KV_MODE</code></td><td>Extracción automática de campos clave=valor (<code>auto</code>, <code>json</code>, <code>xml</code>, <code>none</code>).</td><td><code>KV_MODE=json</code> habilita extracción automática de todos los campos de un evento JSON sin necesidad de <code>spath</code> manual en cada búsqueda.</td></tr><tr><td><code>INDEXED_EXTRACTIONS</code></td><td>Extrae campos <strong>en tiempo de indexación</strong> (no en tiempo de búsqueda) para formatos estructurados (<code>json</code>, <code>csv</code>, <code>w3c</code>). Más rápido en búsqueda, pero aumenta el tamaño del índice (<code>.tsidx</code>) porque cada campo extra se indexa.</td><td>Fuentes de alto volumen y estructura fija (por ejemplo, CSV de un SIEM de terceros) donde el rendimiento de búsqueda es crítico.</td></tr><tr><td><code>TRUNCATE</code></td><td>Máximo de caracteres por evento antes de truncarlo.</td><td>Eventos anómalamente largos (por ejemplo, un log corrupto) que podrían inflar el índice o romper el parsing.</td></tr></tbody></table>

> **Cuidado al combinar `KV_MODE=json` con `INDEXED_EXTRACTIONS=json` en el mismo sourcetype.** Si `INDEXED_EXTRACTIONS=json` ya extrae los campos en tiempo de indexación, dejar además `KV_MODE=json` puede hacer que Splunk **repita la extracción en tiempo de búsqueda** sobre los mismos campos, lo que en el mejor caso es trabajo redundante (impacto en rendimiento) y en el peor puede producir duplicidad o inconsistencias si ambas extracciones no coinciden exactamente. Regla práctica: si usas `INDEXED_EXTRACTIONS=json`, establece `KV_MODE=none` en el mismo `props.conf` para evitar la doble extracción, salvo que tengas una razón concreta y validada para mantener ambas.

#### 54.4 `transforms.conf`: transformación y enrutamiento

Mientras `props.conf` decide **cómo leer** un evento, `transforms.conf` decide **qué hacer con campos concretos o con el enrutamiento**:

```
# transforms.conf - ejemplo: enmascarar un campo sensible antes de indexar
[mask_ssn]
REGEX= (\d{3}-\d{2})-(\d{4})
FORMAT= $1-XXXX
DEST_KEY= _raw

# transforms.conf - ejemplo: extracción de campo en tiempo de búsqueda
[extract_user]
REGEX= user=(?<user>[^\s]+)
```

```
# props.conf referenciando el transform
[mi_sourcetype]
SEDCMD-mask_ssn= s/(\d{3}-\d{2})-(\d{4})/\1-XXXX/g
REPORT-extract_user= extract_user
```

* `SEDCMD` en `props.conf` aplica sustituciones tipo `sed` **en tiempo de indexación** (irreversible: el dato original no se guarda). Se usa para enmascarar PII/secretos antes de que el evento se persista.
* `REPORT-*` en `props.conf` referencia un stanza de `transforms.conf` para extracción de campos **en tiempo de búsqueda** (no aumenta el tamaño del índice, pero consume CPU en cada búsqueda que necesite ese campo).
* `TRANSFORMS-*` (distinto de `REPORT-*`) se usa típicamente para **enrutamiento** de eventos a índices distintos o para descartar eventos (`nullQueue`) según su contenido.

Para no mezclar conceptos, conviene separar claramente qué directivas actúan **en tiempo de indexación** (modifican o deciden el destino del dato de forma permanente) de las que actúan **en tiempo de búsqueda** (no tocan el dato almacenado, solo cómo se interpreta al consultarlo):

| Momento                                                                   | Directivas                                                      | Efecto                                                                                                                                                                    |
| ------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Index-time** (`transforms.conf` aplicado durante la indexación)         | `SEDCMD-*`, `TRANSFORMS-*` (enrutamiento/descarte), `nullQueue` | Modifican o descartan el evento **antes** de escribirlo en el índice. Son irreversibles: si el dato se enmascara o se descarta aquí, no hay forma de recuperarlo después. |
| **Search-time** (aplicado en cada búsqueda, sin tocar el dato almacenado) | `REPORT-*`, `EXTRACT-*`, `FIELDALIAS-*`, `EVAL-*`, `LOOKUP-*`   | Generan campos calculados/derivados en el momento de la búsqueda. Se pueden corregir o añadir en cualquier momento sin re-indexar los datos históricos.                   |

> Regla práctica: si necesitas **corregir** una extracción de campos, hazlo con directivas search-time (`EXTRACT-*`/`REPORT-*`/`EVAL-*`) - se aplican retroactivamente a datos ya indexados. Si necesitas **enmascarar, descartar o enrutar** datos por cumplimiento normativo, usa directivas index-time (`SEDCMD`/`TRANSFORMS-*`/`nullQueue`) - pero recuerda que son permanentes y no se pueden aplicar retroactivamente a datos ya indexados sin reingestar.

#### 54.5 Por qué esto le importa a un Detection Engineer

* Una detección que depende de un campo (`user`, `src_ip`) que **no está bien extraído** en `props.conf`/`transforms.conf` fallará silenciosamente - no dará error, simplemente no encontrará el campo o lo encontrará vacío.
* Antes de escribir una detección sobre una fuente nueva, valida con `| fieldsummary` o `| table _raw` que los campos esperados existen y están bien poblados.
* Si una fuente tiene mucho volumen y se usa en detecciones de alta frecuencia, considera `INDEXED_EXTRACTIONS` o acelerar vía Data Model (sección [42](#id-42.-data-models-en-profundidad)) en vez de depender de extracción en tiempo de búsqueda para cada ejecución.

### 55. CIM Validation

Tener campos mapeados a CIM (sección [47](#id-47.-cim-mapping-de-campo-original-a-campo-cim)) no garantiza que el mapeo esté **completo y correcto**. Esta sección cubre cómo validar formalmente la cobertura CIM de una fuente.

#### 55.1 Verificar cobertura de datos en un Data Model

```
| tstats count FROM datamodel=Authentication.Authentication WHERE index=* BY sourcetype
```

Si un `sourcetype` que debería aportar datos de autenticación (por ejemplo `WinEventLog:Security`) no aparece en el resultado, significa que **no está tageado** correctamente hacia el data model (falta el `eventtype`/tag CIM correspondiente, normalmente definido en un Add-on o en `eventtypes.conf`/`tags.conf`).

#### 55.2 Verificar población de campos concretos

```
| datamodel Authentication Authentication search
| fieldsummary
| table field count distinct_count
```

Esto ejecuta la búsqueda base del data model (sin aceleración) y resume, campo a campo, cuántos eventos lo tienen poblado (`count`) y cuántos valores distintos toma (`distinct_count`). Un campo CIM importante (`user`, `action`, `src`) con `count` muy bajo respecto al total de eventos indica un problema de mapeo.

#### 55.3 Verificar estado de la aceleración vía REST

```
| rest /services/data-model/model splunk_server=local
| search title="Authentication"
| table title acceleration.enabled acceleration.earliest_time build_time
```

Esto expone el estado de configuración de la aceleración (habilitada/no, ventana de tiempo acelerada, última vez que se construyó el resumen `.tsidx` acelerado). Útil para diagnosticar por qué `summariesonly=true` (sección [42](#id-42.-data-models-en-profundidad)) devuelve menos resultados de los esperados: normalmente porque la ventana acelerada no cubre el rango de tiempo consultado, o porque la construcción del resumen aún no ha terminado.

#### 55.4 Checklist de validación CIM

| Elemento a validar   | Cómo verificarlo                                                                                                                                           |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Coverage %**       | `\| tstats count FROM datamodel=<DM> BY sourcetype` - compara sourcetypes presentes vs. los que deberían aportar datos.                                    |
| **Acceleration %**   | `\| rest /services/data-model/model` - revisar `acceleration.enabled` y `build_time` (¿está al día?).                                                      |
| **Fields populated** | `\| datamodel <DM> <objeto> search \| fieldsummary` - campos CIM clave con `count` bajo son mapeos incompletos.                                            |
| **Tags**             | `\| eventtypes list \| search tags=*` o revisar `tags.conf` - confirmar que el `eventtype` correcto (`authentication`, `success`/`failure`) está aplicado. |
| **Eventtypes**       | `\| eventtypes list` - confirmar que el eventtype asociado a la fuente existe y su búsqueda base (`search`) es correcta.                                   |

> Buena práctica: ejecuta esta validación **cada vez que se onboardea una fuente nueva** relevante para detecciones basadas en CIM/Data Models, y documenta el resultado como parte del checklist de Production Readiness (sección [51](#id-51.-production-readiness-checklist)).

***

### 56. Detection Lifecycle

Escribir una detección (sección [39](#id-39.-metodologia-de-detection-engineering)) es solo el primer paso. Mantener un programa de 200-500+ detecciones vivas requiere un **ciclo de vida gestionado**, igual que el software.

#### 56.1 Las 6 fases

```
Draft → Testing → Production → Monitoring → Tuning → Retirement
  ↑___________________________________________________|
              (una detección puede volver a Tuning/Testing
               cuantas veces sea necesario antes de Retirement)
```

| Fase           | Qué ocurre                                                                                                                                                                                                                                     | Criterio de salida                                                                                                                                       |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Draft**      | Se redacta la hipótesis, se mapea a MITRE, se escribe el SPL inicial (sección [39](#id-39.-metodologia-de-detection-engineering)).                                                                                                             | El SPL corre sin errores de sintaxis y produce resultados sobre datos históricos.                                                                        |
| **Testing**    | Se valida contra datos sintéticos y/o Atomic Red Team (secciones [49](#id-49.-purple-teaming-deteccion-basada-en-atomic-red-team) y [50](#id-50.-testing-de-detecciones)). Se mide TP/FP en un entorno controlado.                             | La detección dispara ante el ataque simulado y no dispara excesivamente ante tráfico normal (FP rate aceptable definido previamente).                    |
| **Production** | Se despliega como alerta/correlation search real, con RBA o notable directo según severidad (sección [40](#id-40.-risk-based-alerting-rba)). Pasa el checklist de Production Readiness (sección [51](#id-51.-production-readiness-checklist)). | Sign-off documentado (owner, fecha, aprobador).                                                                                                          |
| **Monitoring** | Fase continua: se registran KPIs (sección [57](#id-57.-kpis-de-soc)) - TPR, FPR, volumen de disparos - y feedback de analistas (¿el notable fue útil? ¿fue FP?).                                                                               | Ninguno - es un estado permanente mientras la detección esté activa.                                                                                     |
| **Tuning**     | Se activa cuando Monitoring revela FPR alto, volumen excesivo, o cobertura insuficiente. Se ajustan umbrales, exclusiones o lógica, y se re-entra en Testing antes de volver a Production.                                                     | El ajuste corrige el problema detectado sin introducir uno nuevo (validado de nuevo en Testing).                                                         |
| **Retirement** | La detección se desactiva formalmente: porque la técnica ya no aplica (cambio de infraestructura), fue sustituida por una versión mejor, o su FPR es inasumible sin solución razonable.                                                        | Documentar motivo de retiro y, si aplica, la detección sustituta, para no perder cobertura silenciosamente (sección [48](#id-48.-coverage-engineering)). |

#### 56.2 Gobernanza del lifecycle

* **Cada detección en producción debe tener un owner** (persona o equipo responsable de su tuning y retirement).
* **Revisión periódica obligatoria:** ninguna detección debería permanecer en Production indefinidamente sin revisión - fija una cadencia (por ejemplo, trimestral) para pasar por Monitoring→Tuning si el volumen de FPs supera el umbral acordado.
* **Registro central:** el `detections_catalog.csv` introducido en la sección [48](#id-48.-coverage-engineering) es el lugar natural para trackear en qué fase del lifecycle está cada detección (añadir columna `lifecycle_stage`), quién es el owner, y la fecha de la última revisión.
* **Nunca borres una detección retirada del catálogo** - márcala como `retired` con la fecha y el motivo, para mantener trazabilidad histórica de cobertura.

***

### 57. KPIs de SOC

Un programa de detección maduro no solo produce alertas: **mide si el SOC está mejorando**. Estos son los KPIs de referencia más habituales, con cómo aproximarlos en SPL.

#### 57.1 Métricas de volumen

```
# Alerts/day (todas las alertas programadas disparadas)
index=_internal source=*scheduler.log status=success earliest=-7d@d
| bin _time span=1d
| stats dc(savedsearch_name) AS unique_alerts count AS alert_fires BY _time

# Notables/day (Enterprise Security)
| tstats count FROM datamodel=Notable WHERE index=notable earliest=-7d@d BY _time span=1d

# Risk Events/day (RBA - sección 40)
index=risk earliest=-7d@d
| bin _time span=1d
| stats count AS risk_events BY _time
```

| KPI                 | Qué mide                                                                                                | Por qué importa                                                                                                                                       |
| ------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Alerts/day**      | Volumen bruto de alertas programadas disparadas.                                                        | Tendencia general de “ruido” del sistema; un aumento súbito sin cambios de contenido sugiere un problema de datos, no un incremento real de amenazas. |
| **Findings/day**    | Hallazgos agregados que un analista revisa (puede incluir agrupaciones de varias alertas relacionadas). | Refleja mejor la carga de trabajo real del analista que el conteo bruto de alertas.                                                                   |
| **Notables/day**    | Notables generados en Enterprise Security (post-correlation search).                                    | Indicador directo de carga operativa L1.                                                                                                              |
| **Risk Events/day** | Eventos de riesgo generados por RBA (antes de convertirse en notable/incidente).                        | Mide el “pulso” de señales de riesgo del entorno independientemente de si llegan a generar un notable.                                                |

#### 57.2 Métricas de calidad de detección

| KPI                                     | Fórmula                                                                                                                                                                                   | Cómo aproximarlo                                                                                                                                                                                          |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **TPR (True Positive Rate)**            | $\text{TPR} = \dfrac{\text{Verdaderos Positivos}}{\text{Verdaderos Positivos} + \text{Falsos Negativos}}$  ·  en texto plano: `TPR = True Positives / (True Positives + False Negatives)` | Requiere disposición/feedback del analista por notable (campo `disposition` en ES: True Positive, Benign, False Positive) - `\| tstats count FROM datamodel=Notable WHERE disposition="*true_positive*"`. |
| **FPR (False Positive Rate)**           | $\text{FPR} = \dfrac{\text{Falsos Positivos}}{\text{Falsos Positivos} + \text{Verdaderos Negativos}}$  ·  en texto plano: `FPR = False Positives / (False Positives + True Negatives)`    | Igual que arriba, filtrando `disposition="*false_positive*"` sobre el total de notables de una detección concreta.                                                                                        |
| **MTTD (Mean Time To Detect)**          | Tiempo medio entre que ocurre el evento origen (`_time` del evento) y que se genera la alerta/notable.                                                                                    | `\| eval detection_delay=alert_time-event_time \| stats avg(detection_delay)` - requiere correlar el `_time` del evento fuente con el timestamp del notable.                                              |
| **MTTR (Mean Time To Respond/Resolve)** | Tiempo medio entre que se genera el notable y se cierra/resuelve.                                                                                                                         | En ES: `\| tstats avg(eval(time_to_resolution)) FROM datamodel=Notable` usando `time_created` y `time_closed` del notable.                                                                                |
| **Coverage %**                          | Ver sección [48](#id-48.-coverage-engineering) - % de técnicas ATT\&CK relevantes con al menos una detección validada.                                                                    | `detections_catalog.csv` cruzado con `mitre_attack_techniques.csv`.                                                                                                                                       |

> **Nota operativa:** las fórmulas clásicas de TPR/FPR requieren conocer los **True Negatives**, algo que en la práctica de un SOC casi nunca se puede calcular con precisión (no existe un conteo fiable de “todo lo que no ocurrió y no se alertó”). Por eso, en la operación diaria es más realista usar una versión basada solo en notables **cerrados** (con disposición ya asignada por el analista):
>
> * `True Positive Ratio = True Positive Notables / Total Closed Notables`
> * `False Positive Ratio = False Positive Notables / Total Closed Notables`
>
> ```
> | tstats count FROM datamodel=Notable WHERE index=notable status=closed earliest=-30d@d BY disposition
> | eventstats sum(count) AS total_closed
> | eval ratio=round(count/total_closed,3)
> ```
>
> Usa las fórmulas clásicas (TPR/FPR con True Negatives) cuando dispongas de un conjunto de datos de validación controlado (por ejemplo, en Testing/Purple Teaming, sección [49](#id-49.-purple-teaming-deteccion-basada-en-atomic-red-team)/[50](#id-50.-testing-de-detecciones)), donde sí conoces el total de eventos negativos inyectados. Usa la versión operativa (Total Closed Notables) para el seguimiento continuo en producción.

#### 57.3 Dashboard mínimo recomendado

Un dashboard de KPIs de SOC debería mostrar, como mínimo: tendencia de notables/día (últimas 4 semanas), FPR por detección (top 10 detecciones más ruidosas), MTTD/MTTR agregados por severidad, y coverage % por táctica ATT\&CK. Esto permite a un responsable de SOC identificar rápidamente **qué detecciones tunear primero** (las de mayor volumen y mayor FPR) y **dónde falta cobertura**.

***

### 58. ES Correlation Searches

Splunk Enterprise Security tiene su propio vocabulario para el ciclo “detectar → agregar → responder”, distinto (aunque relacionado) del RBA puro de la sección [40](#id-40.-risk-based-alerting-rba).

#### 58.1 Conceptos clave

| Concepto                             | Qué es                                                                                                                                                                                                                                                                                                          |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Correlation Search**               | Una búsqueda programada especial de ES que, cuando produce resultados, puede generar un **Notable Event** y/o disparar **Adaptive Responses**. Es el equivalente de una “regla de detección” en el lenguaje nativo de ES.                                                                                       |
| **Adaptive Response (AR)**           | Una acción automática que se ejecuta cuando una correlation search dispara: crear un notable, ejecutar un script, enriquecer con threat intel, bloquear una IP en un firewall (vía app de terceros), notificar por email/Slack, etc. Se configuran en la pestaña “Adaptive Responses” de la correlation search. |
| **Notable Event (Notable)**          | El “ticket” que ve un analista L1 en el panel de Incident Review. Contiene la evidencia, severidad, urgencia (calculada a partir de severidad + prioridad del asset/identity, sección [59](#id-59.-asset-and-identity-framework)), y campos de disposición para el triage.                                      |
| **Risk Rule**                        | Una correlation search especial cuyo Adaptive Response es escribir un **Risk Event** al índice `risk` en vez de (o además de) generar un notable directo - es el mecanismo concreto que implementa RBA (sección [40](#id-40.-risk-based-alerting-rba)).                                                         |
| **Episode Review / Mission Control** | La interfaz moderna de ES (sucesora del Incident Review clásico) que agrupa notables y risk events relacionados en **episodios**, permitiendo triage, investigación y respuesta desde una vista unificada, con integración de playbooks de respuesta (SOAR).                                                    |

#### 58.2 Relación entre los conceptos

```
Correlation Search (SPL programado)
        │
        ├── Adaptive Response: "Notable" ─────────► Notable Event ─────► Episode Review / Mission Control
        │                                                                        (triage por analista)
        └── Adaptive Response: "Risk Analysis" ───► Risk Event (índice risk)
                                                              │
                                                     Risk Incident Rule (agrega risk events)
                                                              │
                                                              ▼
                                                     Notable (si risk_score acumulado > umbral)
```

* Una correlation search **puede** generar un notable directamente (patrón clásico, para señales de alta confianza) **o** alimentar RBA generando risk events que solo se convierten en notable cuando se acumula suficiente riesgo (patrón moderno, para señales de confianza media/baja que se combinan).
* La mayoría de programas maduros usan **ambos**: notables directos para detecciones de alta confianza y bajo volumen (Golden Ticket, DCShadow - sección [53](#id-53.-anexo-enterprise-security-detections-library)), y RBA para señales que solo son significativas en combinación (PowerShell sospechoso, login desde IP nueva, etc.).

#### 58.3 Ejemplo de configuración conceptual de una Correlation Search

```
Nombre:               "SOC - Password Spray Detected - Rule"
Búsqueda base:        [SPL de la sección 53.1]
Cron schedule:        */15 * * * *  (cada 15 minutos)
Time range:           earliest=-15m@m latest=-5m@m
Throttling:           por risk_object (evitar duplicar notables para el mismo src_ip en la misma hora)
Adaptive Response 1:  "Risk Analysis" → risk_object=src_ip, risk_score=50
Adaptive Response 2:  "Notable" (solo si se quiere notable directo además de RBA) → severity=medium
```

* **Throttling** es crítico: sin él, la misma correlation search puede generar un notable duplicado cada vez que se ejecuta mientras la condición siga siendo cierta. Se configura por combinación de campos (típicamente el `risk_object`) y una ventana de tiempo.
* El **severity** del notable en ES no es fijo: se calcula combinando la severidad de la correlation search con la **prioridad del asset/identity** involucrado (sección [59](#id-59.-asset-and-identity-framework)) para producir la **urgency** final que ve el analista.

***

### 59. Asset & Identity Framework

ES enriquece automáticamente cualquier evento/notable que contenga un campo `src`/`dest` (IP o hostname) o `user`/`identity` con metadata de dos lookups especiales: **Assets** e **Identities**. Esto no es solo “otro lookup” - alimenta directamente el cálculo de severidad/urgencia y el scoring de RBA.

#### 59.1 `assets.csv` - inventario de sistemas

Campos típicos que ES espera/reconoce en el framework de Assets:

<table><thead><tr><th width="271">Campo</th><th>Significado</th></tr></thead><tbody><tr><td><code>ip</code> / <code>dns</code> / <code>mac</code> / <code>nt_host</code></td><td>Identificadores del asset (al menos uno requerido para hacer match).</td></tr><tr><td><code>category</code></td><td>Etiquetas libres (<code>servidor</code>, <code>endpoint</code>, <code>dmz</code>, <code>critico</code>) usadas para segmentar y para reglas de prioridad.</td></tr><tr><td><code>priority</code></td><td>Prioridad de negocio del asset: <code>critical</code>, <code>high</code>, <code>medium</code>, <code>low</code>. Es el campo que más impacta en el cálculo de urgencia.</td></tr><tr><td><code>owner</code></td><td>Responsable del sistema - útil para enrutar la investigación/respuesta.</td></tr><tr><td><code>is_expected</code></td><td>Si el asset tiene un comportamiento base conocido (útil para reducir FPs en detecciones de anomalías).</td></tr></tbody></table>

#### 59.2 `identities.csv` - inventario de personas/cuentas

<table><thead><tr><th width="267">Campo</th><th>Significado</th></tr></thead><tbody><tr><td><code>identity</code> / <code>nick</code></td><td>Identificadores de usuario (nombre, sAMAccountName, email - al menos uno requerido).</td></tr><tr><td><code>priority</code></td><td>Igual que en assets: <code>critical</code>/<code>high</code>/<code>medium</code>/<code>low</code>.</td></tr><tr><td><code>category</code></td><td>Etiquetas libres, incluyendo las especiales reconocidas por ES:</td></tr><tr><td><code>watchlist</code></td><td>Marca a la identidad como “bajo vigilancia” (por ejemplo, empleado en proceso de salida, cuenta con historial de incidentes).</td></tr><tr><td><code>vip</code></td><td>Marca ejecutivos/personal de alto perfil - su compromiso tiene mayor impacto reputacional/de negocio.</td></tr><tr><td><code>privileged</code></td><td>Marca cuentas con privilegios elevados (administradores de dominio, cuentas de servicio críticas).</td></tr></tbody></table>

#### 59.3 Cómo afecta al Risk Score y a la Severity/Urgency

En Enterprise Security, la **urgencia** final de un notable no es solo la severidad de la correlation search: se calcula como una combinación de **severidad** (definida en la correlation search) × **prioridad del asset/identity** (definida en los frameworks anteriores). Un mismo hallazgo técnico genera urgencia distinta si afecta a un servidor `low priority` de laboratorio o a un Domain Controller `critical`.

De forma equivalente, en RBA (sección [40](#id-40.-risk-based-alerting-rba)) es una práctica común **multiplicar o sumar el risk\_score** según la prioridad de la entidad:

```
| lookup identity_lookup_expanded identity AS user OUTPUT priority AS identity_priority, watchlist, vip, privileged
| eval priority_multiplier=case(
    identity_priority="critical", 2.0,
    identity_priority="high", 1.5,
    identity_priority="medium", 1.0,
    true(), 0.75)
| eval risk_score=round(base_risk_score * priority_multiplier, 0)
| eval risk_score=if(privileged="true" OR vip="true", risk_score + 20, risk_score)
```

> Ejemplo: la misma detección de “Suspicious PowerShell” (`base_risk_score=30`) resultaría en `risk_score=60` si el usuario es `privileged=true`, frente a `risk_score=30` para un usuario estándar - reflejando que el mismo comportamiento técnico es más peligroso en una cuenta con privilegios.

#### 59.4 Buenas prácticas de mantenimiento

* **Automatiza la actualización** de `assets.csv`/`identities.csv` desde la fuente de verdad (CMDB, Active Directory/Entra ID, HRIS) en vez de mantenerlos manualmente - es la causa más común de que este framework quede desactualizado y pierda valor.
* Un asset/identity **sin match** en estos lookups no falla la detección, pero pierde el enriquecimiento de prioridad - revisa periódicamente el % de eventos con match (similar al ejercicio de Coverage Engineering, sección [48](#id-48.-coverage-engineering)) para detectar huecos de cobertura del inventario.

***

### 60. Sigma → SPL

**Sigma** es un formato abierto (YAML) para escribir reglas de detección de forma agnóstica al SIEM, pensado para poder traducirse a la sintaxis nativa de distintas plataformas (Splunk SPL, Elastic, Microsoft Sentinel KQL, etc.). Muchas organizaciones y fuentes de inteligencia de detección (SigmaHQ, comunidad de threat hunting) publican reglas en este formato.

#### 60.1 Estructura de una regla Sigma

```yaml
title: Kerberoasting via Suspicious RC4 TGS Request
id: 8ac60395-2010-4dd6-8ff9-a367e5f01cd4
status: stable
logsource:
product: windows
service: security
detection:
selection:
EventID:4769
Ticket_Encryption_Type:'0x17'
condition: selection
timeframe: 1h
level: high
tags:
- attack.credential_access
- attack.t1558.003
```

#### 60.2 Pipeline Sigma → SPL → Correlation Search → RBA

```
Regla Sigma (YAML, agnóstica)
        │  (traducción manual, o con herramientas como pySigma / sigma-cli con backend de Splunk)
        ▼
SPL equivalente
        │  (se envuelve como Correlation Search en ES - sección 58)
        ▼
Correlation Search (programada, con Adaptive Response)
        │  (el Adaptive Response escribe un Risk Event en vez de/además de un notable)
        ▼
RBA (Risk Event → Risk Incident Rule → Notable si se supera el umbral - sección 40)
```

#### 60.3 Ejemplo de traducción manual

La regla Sigma de Kerberoasting de 60.1 se traduce directamente a la detección ya documentada en la sección [53.7](#id-53.7-kerberoasting):

```
index=windows sourcetype=WinEventLog:Security EventCode=4769 earliest=-1h
| where Ticket_Encryption_Type="0x17"
| stats count dc(Service_Name) AS distinct_spns BY Account_Name
| where distinct_spns >= 5
```

Nótese que la regla Sigma original **no incluye el umbral `distinct_spns >= 5`** - ese refinamiento (necesario para reducir falsos positivos frente a la regla Sigma “cruda”, que dispararía con un solo evento 4769 con RC4) es exactamente el tipo de *tuning* que aporta un Detection Engineer al adaptar una regla genérica de la comunidad a un entorno concreto (ver sección [45](#id-45.-guia-de-reduccion-de-falsos-positivos)).

#### 60.4 Consideraciones prácticas

* **No traduzcas Sigma a SPL de forma ciega.** Las reglas Sigma suelen estar escritas para ser lo más genéricas posible; casi siempre necesitan umbral/tuning adicional para el volumen y contexto de tu entorno (igual que el ejemplo anterior).
* Herramientas como `sigma-cli`/`pySigma` (proyecto open source de SigmaHQ) automatizan gran parte de la traducción sintáctica, pero **la validación (sección** [**50**](#id-50.-testing-de-detecciones)**) sigue siendo manual y obligatoria** antes de pasar a producción.
* Aprovecha el campo `tags` de Sigma (formato `attack.tXXXX`) para poblar automáticamente el campo `mitre_technique_id` de tu `detections_catalog.csv` (sección [48](#id-48.-coverage-engineering)) al importar una regla.
* Mantén una referencia al `id` de la regla Sigma original en tu catálogo interno - facilita actualizar la detección si SigmaHQ publica una revisión de la regla.

***

### 61. Attack Chains: encadenando técnicas ATT\&CK

Las secciones [44](#id-44.-mapeo-att-and-ck-para-detecciones-spl) y [53](#id-53.-anexo-enterprise-security-detections-library) tratan las técnicas ATT\&CK de forma individual. En un ataque real, las técnicas se **encadenan**: el valor de RBA (sección [40](#id-40.-risk-based-alerting-rba)) está precisamente en detectar la acumulación de riesgo a lo largo de esta cadena, no solo una técnica aislada.

#### 61.1 Kill chain de ejemplo: de credenciales a exfiltración

```
1. Password Spray            →  2. Valid Account          →  3. Suspicious PowerShell
   (T1110.003)                     (T1078)                      (T1059.001)
        │                               │                              │
        ▼                               ▼                              ▼
4. Privilege Escalation      →  5. Kerberoasting          →  6. Golden Ticket
   (T1098)                         (T1558.003)                  (T1558.001)
        │                               │                              │
        └───────────────────────────────┴──────────────────────────────┘
                                         ▼
                              7. DNS Exfiltration
                                 (T1048.003)
```

<table data-search="false"><thead><tr><th>#</th><th>Técnica</th><th>MITRE</th><th>Log fuente</th><th>SPL de referencia</th><th>RBA</th></tr></thead><tbody><tr><td>1</td><td>Password Spray</td><td>T1110.003</td><td><code>WinEventLog:Security</code> EventCode=4625</td><td>Sección <a href="#id-53.1-password-spray">53.1</a></td><td><code>risk_score=50</code></td></tr><tr><td>2</td><td>Valid Account (uso exitoso tras spray)</td><td>T1078</td><td><code>WinEventLog:Security</code> EventCode=4624</td><td>`... EventCode=4624</td><td>search [subsearch de src_ip con spray previo]`</td></tr><tr><td>3</td><td>Suspicious PowerShell</td><td>T1059.001</td><td>Sysmon EventID=1 / EventCode=4688</td><td>Sección <a href="#id-20.3-powershell-sospechoso">20.3</a> / <a href="#id-41.2-powershell-hunting">41.2</a></td><td><code>risk_score=30</code></td></tr><tr><td>4</td><td>Privilege Escalation</td><td>T1098</td><td><code>WinEventLog:Security</code> EventCode IN (4728,4732,4756)</td><td>Sección <a href="#id-53.6-privilege-escalation">53.6</a></td><td><code>risk_score=50</code></td></tr><tr><td>5</td><td>Kerberoasting</td><td>T1558.003</td><td><code>WinEventLog:Security</code> EventCode=4769</td><td>Sección <a href="#id-53.7-kerberoasting">53.7</a></td><td><code>risk_score=60</code></td></tr><tr><td>6</td><td>Golden Ticket</td><td>T1558.001</td><td><code>WinEventLog:Security</code> EventCode=4768</td><td>Sección <a href="#id-53.9-golden-ticket">53.9</a></td><td><code>risk_score=95</code></td></tr><tr><td>7</td><td>DNS Exfiltration</td><td>T1048.003</td><td><code>index=dns</code></td><td>Sección <a href="#id-53.10-dns-exfiltration">53.10</a></td><td><code>risk_score=55</code></td></tr></tbody></table>

#### 61.2 Por qué esto importa más que la suma de las partes

Cada técnica individual, aislada, puede tener un `risk_score` moderado y no justificar por sí sola un notable (para evitar ruido - sección [45](#id-45.-guia-de-reduccion-de-falsos-positivos)). Pero **la misma entidad (`user` o `src_ip`) acumulando riesgo de varias etapas de esta cadena en una ventana de tiempo corta** es una señal de compromiso mucho más fuerte que cualquier evento individual - exactamente el principio central de RBA.

```
# Ejemplo de Risk Incident Rule que detecta la acumulación de la cadena completa
| tstats sum(All_Risk.risk_score) AS total_risk
    values(All_Risk.annotations.mitre_attack.mitre_technique_id) AS techniques_seen
    dc(All_Risk.annotations.mitre_attack.mitre_technique_id) AS distinct_techniques
  FROM datamodel=Risk.All_Risk
  WHERE earliest=-24h
  BY All_Risk.risk_object
| where distinct_techniques >= 3 AND total_risk >= 150
| sort - total_risk
```

Esta query es, en esencia, una **Risk Incident Rule** simplificada: agrega risk events de múltiples técnicas distintas por entidad (`risk_object`) y solo genera un resultado (candidato a notable) cuando se observan **al menos 3 técnicas distintas de la cadena** con un riesgo acumulado significativo - mucho más específico que cualquier alerta individual.

#### 61.3 Cómo construir tus propias attack chains

1. Parte de un informe de threat intelligence o de un pentest/red team real que describa una intrusión completa (kill chain observada).
2. Mapea cada etapa observada a una técnica ATT\&CK y a una detección existente (o crea una nueva siguiendo la sección [39](#id-39.-metodologia-de-detection-engineering)).
3. Verifica, con Purple Teaming (sección [49](#id-49.-purple-teaming-deteccion-basada-en-atomic-red-team)), que cada etapa dispara su risk event correctamente.
4. Define una Risk Incident Rule (como el ejemplo anterior) que agregue las etapas relevantes de esa cadena concreta, en vez de depender únicamente de la agregación genérica por `risk_object` y umbral total.
5. Documenta la cadena completa en el catálogo de detecciones (sección [48](#id-48.-coverage-engineering)) como una entidad propia (“Detección de Cadena”), no solo como técnicas sueltas - esto también mejora directamente el Coverage Engineering al evidenciar cobertura de **secuencias**, no solo de técnicas aisladas.

***

### 62. Cloud Detection Library

Complementa la sección [53](#id-53.-anexo-enterprise-security-detections-library) con detecciones específicas para los proveedores cloud ya cubiertos como fuentes en la sección [46](#id-46.-common-soc-data-sources) (AWS CloudTrail, Azure AD, Microsoft 365), siguiendo el mismo formato Hipótesis/MITRE/SPL/Tuning/FPs/RBA/Entidad/Volumen/Investigación.

#### 62.1 AWS Root Login

* **Hipótesis:** el uso de la cuenta **root** de AWS (en vez de un rol/usuario IAM con privilegios mínimos) es prácticamente siempre anómalo en operación normal - la cuenta root debería reservarse para tareas administrativas excepcionales con MFA obligatorio.
* **MITRE:** T1078.004 (Valid Accounts: Cloud Accounts).
* **SPL:**

```
index=aws sourcetype=aws:cloudtrail userIdentity.type=Root earliest=-1h
| table _time eventName sourceIPAddress userIdentity.accountId responseElements.ConsoleLogin
```

* **Tuning:** ninguno realista - cualquier login root fuera de una ventana de mantenimiento planificada y documentada merece revisión.
* **FPs:** tareas de mantenimiento de cuenta que requieren explícitamente root (cambios de plan de soporte, cierre de cuenta) - deben estar documentadas de antemano.
* **RBA:** `risk_object=userIdentity.accountId`, `risk_score=80`.
* **Entidad esperada:** la cuenta AWS (`accountId`) como `risk_object`.
* **Volumen esperado:** 0 en la inmensa mayoría de organizaciones bien configuradas (que fuerzan uso de roles IAM); cualquier disparo debe investigarse manualmente sin importar el volumen.
* **Pasos de investigación:** (1) confirmar con el equipo de cloud/infraestructura si el login estaba planificado; (2) revisar `sourceIPAddress` contra rangos corporativos conocidos; (3) si no está documentado, rotar credenciales root y habilitar/verificar MFA de forma inmediata; (4) revisar CloudTrail de las horas siguientes en busca de cambios de IAM o de facturación.

#### 62.2 AWS AccessKey Creation

* **Hipótesis:** la creación de nuevas access keys para un usuario IAM, especialmente fuera de un proceso de aprovisionamiento conocido, puede indicar persistencia tras un compromiso inicial (el atacante crea una credencial propia de larga duración).
* **MITRE:** T1098.001 (Account Manipulation: Additional Cloud Credentials).
* **SPL:**

```
index=aws sourcetype=aws:cloudtrail eventName=CreateAccessKey earliest=-1h
| lookup iam_provisioning_tickets.csv requestParameters.userName AS target_user OUTPUT ticket_id
| where isnull(ticket_id)
| table _time userIdentity.arn requestParameters.userName responseElements.accessKey.accessKeyId sourceIPAddress
```

* **Tuning:** requiere un lookup de “tickets de aprovisionamiento IAM conocidos” para distinguir altas legítimas de sospechosas; sin él, esta detección generará ruido en entornos con rotación frecuente de claves.
* **FPs:** rotación programada de credenciales por políticas de seguridad, onboarding de nuevos servicios/aplicaciones.
* **RBA:** `risk_object=requestParameters.userName`, `risk_score=55`.
* **Entidad esperada:** el usuario IAM objetivo (`requestParameters.userName`) como `risk_object`; `userIdentity.arn` (quien ejecutó la acción) como contexto.
* **Volumen esperado:** depende del proceso de rotación de claves de la organización; establecer baseline y usar el lookup de tickets para aislar el ruido esperado.
* **Pasos de investigación:** (1) verificar si existe ticket de cambio para la creación de esta clave; (2) revisar el historial reciente de `userIdentity.arn` en CloudTrail (¿tuvo actividad anómala previa?); (3) si no está autorizado, desactivar/eliminar la access key inmediatamente (`aws iam update-access-key --status Inactive`); (4) revisar si la clave nueva ya se usó (`eventName=CreateAccessKey` seguido de llamadas autenticadas con esa `accessKeyId`).

#### 62.3 AWS AssumeRole Abuse

* **Hipótesis:** un atacante que compromete una identidad con permisos limitados intenta escalar privilegios asumiendo (`sts:AssumeRole`) un rol más privilegiado al que no debería tener acceso rutinario, o encadena múltiples `AssumeRole` para dificultar la trazabilidad (“role chaining”).
* **MITRE:** T1548.005 (Abuse Elevation Control Mechanism: Temporary Elevated Cloud Access) / T1078.004.
* **SPL:**

```
index=aws sourcetype=aws:cloudtrail eventName=AssumeRole earliest=-1h
| stats dc(requestParameters.roleArn) AS distinct_roles values(requestParameters.roleArn) AS roles_assumed BY userIdentity.arn
| where distinct_roles >= 4
```

* **Tuning:** ajustar el umbral de `distinct_roles` según el patrón normal de la organización (equipos de plataforma/CI-CD legítimamente asumen múltiples roles); excluir ARNs de pipelines de automatización conocidos.
* **FPs:** pipelines de CI/CD, herramientas de automatización de infraestructura (Terraform, CloudFormation con roles de despliegue) que asumen múltiples roles por diseño.
* **RBA:** `risk_object=userIdentity.arn`, `risk_score=60`, mayor si alguno de los `roles_assumed` es un rol administrativo conocido.
* **Entidad esperada:** el `userIdentity.arn` origen como `risk_object`.
* **Volumen esperado:** bajo para identidades humanas; alto (y esperado) para roles de automatización - de ahí la importancia de segmentar por tipo de identidad antes de aplicar el umbral.
* **Pasos de investigación:** (1) confirmar si `userIdentity.arn` corresponde a una identidad humana o de automatización; (2) revisar la lista de `roles_assumed` en busca de roles administrativos o con acceso a datos sensibles; (3) correlar con el resto de la cadena (sección [61](#id-61.-attack-chains-encadenando-tecnicas-att-and-ck)) - ¿hubo un `CreateAccessKey` o login sospechoso previo de esta misma identidad?; (4) revocar sesiones activas (`aws sts` no permite revocar directamente - requiere invalidar credenciales/rotar policy) si se confirma abuso.

#### 62.4 Azure Consent Grant (Illicit Consent Grant)

* **Hipótesis:** un atacante engaña a un usuario (vía phishing) para que conceda permisos OAuth a una aplicación maliciosa registrada en Azure AD/Entra ID, obteniendo así acceso persistente a datos (correo, archivos) sin necesidad de robar la contraseña.
* **MITRE:** T1528 (Steal Application Access Token).
* **SPL:**

```
index=azuread sourcetype=azure:aad:audit earliest=-1h
operationName="Consent to application"
| eval requested_permissions=mvjoin('modifiedProperties{}.newValue', ", ")
| where match(requested_permissions, "(?i)mail\.read|files\.readwrite|offline_access")
| table _time initiatedBy.user.userPrincipalName targetResources{}.displayName requested_permissions
```

* **Tuning:** mantener una lista blanca de aplicaciones corporativas aprobadas (`known_approved_apps.csv`) para excluir consentimientos legítimos y reducir ruido; prestar especial atención al scope `offline_access` (permite acceso persistente sin reautenticación).
* **FPs:** adopción legítima de nuevas aplicaciones SaaS por parte de usuarios/equipos, integradas correctamente vía el proceso de aprobación de aplicaciones de la organización.
* **RBA:** `risk_object=initiatedBy.user.userPrincipalName`, `risk_score=65`, mayor si el scope incluye `offline_access` o permisos de correo/archivos.
* **Entidad esperada:** el usuario que otorgó el consentimiento como `risk_object`; el nombre de la aplicación (`targetResources{}.displayName`) como campo de contexto crítico para la respuesta.
* **Volumen esperado:** bajo-medio, dependiendo de cuánto se fomente la adopción de apps de terceros en la organización; cualquier disparo sobre una app no reconocida merece revisión.
* **Pasos de investigación:** (1) identificar la aplicación exacta (nombre, publisher, URL de redirección) en el registro de Azure AD; (2) revisar si el publisher es verificado y si la app tiene reputación conocida; (3) si es maliciosa, revocar el consentimiento (`Revoke-AzureADOAuth2PermissionGrant` o desde el portal de Enterprise Applications) y notificar/resetear al usuario afectado; (4) revisar actividad de correo/archivos del usuario en busca de exfiltración ya ocurrida a través de la app.

#### 62.5 Azure Impossible Travel

Ver desarrollo completo en la sección [53.2](#id-53.2-impossible-travel) - la detección y su lógica de distancia/velocidad implícita aplican igualmente a Azure AD/Entra ID como fuente. MITRE: **T1078** (Valid Accounts). RBA sugerido: `risk_score=60`.

#### 62.6 M365 Inbox Rule (creación de reglas de reenvío sospechosas)

* **Hipótesis:** tras comprometer un buzón de Microsoft 365, un atacante crea una regla de reenvío/eliminación automática (por ejemplo, reenviar correos a una dirección externa y borrarlos de la bandeja) para exfiltrar correo de forma silenciosa y sostenida.
* **MITRE:** T1114.003 (Email Collection: Email Forwarding Rule).
* **SPL:**

```
index=o365 sourcetype=o365:management:activity Workload=Exchange Operation IN (New-InboxRule, Set-InboxRule) earliest=-1h
| eval forwards_external=if(match(Parameters, "(?i)ForwardTo|RedirectTo"), "true", "false")
| where forwards_external="true"
| table _time UserId Parameters ClientIP
```

* **Tuning:** excluir reglas de reenvío hacia dominios corporativos conocidos (fusiones, alias internos) mediante un lookup de dominios propios; distinguir reglas creadas por el propio usuario (`ClientIP` habitual) de las creadas desde una sesión/IP anómala.
* **FPs:** empleados configurando reenvío legítimo a una cuenta personal aprobada, asistentes configurando reglas para sus jefes.
* **RBA:** `risk_object=UserId`, `risk_score=70` si el destino de reenvío es un dominio externo no corporativo.
* **Entidad esperada:** el buzón/usuario (`UserId`) como `risk_object`.
* **Volumen esperado:** bajo; establecer baseline por usuario/departamento y prestar especial atención a reglas creadas fuera del horario laboral habitual del usuario.
* **Pasos de investigación:** (1) revisar el destino exacto del reenvío y si el dominio es conocido/confiable; (2) comprobar si hubo un login sospechoso (impossible travel, IP no habitual) justo antes de la creación de la regla - indicaría la cadena compromiso→persistencia; (3) eliminar la regla de reenvío inmediatamente si es maliciosa; (4) forzar rotación de credenciales y revisar el buzón en busca de correos ya exfiltrados.

#### 62.7 M365 Mass Download / Mass File Access

* **Hipótesis:** un atacante (o un insider) con acceso a un buzón/cuenta de OneDrive/SharePoint comprometida descarga o accede a un volumen anómalamente alto de archivos en un periodo corto, indicando exfiltración de datos masiva.
* **MITRE:** T1530 (Data from Cloud Storage) / T1114.002 (Email Collection: Remote Email Collection, si aplica a buzón).
* **SPL:**

```
index=o365 sourcetype=o365:management:activity Workload=SharePoint Operation IN (FileDownloaded, FileAccessed) earliest=-1h
| stats count AS file_events dc(SourceFileName) AS distinct_files BY UserId, ClientIP
| where distinct_files >= 100
```

* **Tuning:** ajustar el umbral de `distinct_files` según el rol del usuario (un administrador de SharePoint o un proceso de migración legítima tendrá volúmenes altos por diseño); excluir cuentas de servicio/migración conocidas mediante lookup.
* **FPs:** sincronización inicial de OneDrive en un equipo nuevo, migraciones de contenido planificadas, herramientas de backup/DLP legítimas que indexan contenido.
* **RBA:** `risk_object=UserId`, `risk_score=65`, mayor si `ClientIP` no es una IP corporativa habitual para ese usuario.
* **Entidad esperada:** el usuario (`UserId`) como `risk_object`; `ClientIP` como contexto para distinguir acceso desde dispositivo corporativo habitual vs. no habitual.
* **Volumen esperado:** prácticamente 0 para el usuario medio fuera de eventos de sincronización inicial conocidos; establecer baseline por rol (los administradores/equipos de datos tendrán un umbral naturalmente más alto).
* **Pasos de investigación:** (1) confirmar si el volumen de descargas corresponde a una actividad planificada (migración, backup); (2) revisar qué tipo de contenido se accedió (`SourceFileName`, sensibilidad de las carpetas/sitios de SharePoint involucrados); (3) correlar con el estado de la sesión (¿hubo impossible travel o consent grant reciente de este usuario?); (4) si se confirma exfiltración, suspender la sesión, revocar tokens y notificar a Legal/Compliance según el tipo de dato accedido.

***

### 63. Analyst Triage Playbooks

Las secciones [53](#id-53.-anexo-enterprise-security-detections-library) y [62](#id-62.-cloud-detection-library) incluyen pasos de investigación dentro de cada detección individual. Esta sección los complementa con **playbooks genéricos por categoría de alerta**, útiles cuando la detección concreta que dispara no está (todavía) en la librería, o como plantilla base para documentar una nueva.

#### 63.1 Formato del playbook

```
Categoría:            <Credential Access | Suspicious Process | Cloud Identity | Data Exfiltration | Privilege Escalation | Mailbox Compromise>
What happened:        <descripción en una frase de qué comportamiento disparó la alerta>
Why it matters:       <impacto potencial si la alerta es un verdadero positivo>
First 5 checks:       <las 5 primeras comprobaciones que debe hacer un analista L1 en los primeros minutos>
Evidence to collect:  <qué campos/logs/artefactos guardar para la investigación y un posible caso legal>
Escalation criteria:  <cuándo pasar el caso a L2/L3 o a Respuesta a Incidentes>
Containment options:  <acciones de contención disponibles y su impacto operativo>
Closure notes:        <qué documentar al cerrar el caso, independientemente del resultado (TP/FP)>
```

#### 63.2 Credential Access

* **What happened:** se detectó actividad compatible con robo o adivinación de credenciales (password spray, brute force, Kerberoasting - secciones [53.1](#id-53.1-password-spray), [53.7](#id-53.7-kerberoasting)).
* **Why it matters:** es habitualmente la primera etapa de una intrusión (ver [Attack Chains](#id-61.-attack-chains-encadenando-tecnicas-att-and-ck)); si tiene éxito, da al atacante una identidad válida para moverse lateralmente sin generar alertas de malware.
* **First 5 checks:** (1) ¿el usuario/cuenta objetivo tuvo un login exitoso tras los intentos fallidos?; (2) ¿el origen (`src_ip`/host) es conocido/corporativo?; (3) ¿la cuenta es privilegiada o de servicio (sección [59](#id-59.-asset-and-identity-framework))?; (4) ¿hay repetición del mismo patrón contra otras cuentas en la misma ventana?; (5) ¿existe actividad de la cuenta inmediatamente después (nuevo login, cambio de MFA)?
* **Evidence to collect:** eventos 4625/4624/4768/4769 relevantes, `src_ip`, user-agent/host de origen, resultado de reputación de IP.
* **Escalation criteria:** login exitoso confirmado tras el patrón de ataque, o la cuenta objetivo es privilegiada/VIP.
* **Containment options:** bloqueo temporal de la cuenta, reseteo forzado de contraseña, bloqueo del `src_ip` en el perímetro.
* **Closure notes:** registrar si fue TP/FP, el umbral que disparó la alerta, y si se requiere tuning (sección [45](#id-45.-guia-de-reduccion-de-falsos-positivos)).

#### 63.3 Suspicious Process

* **What happened:** ejecución de un proceso/binario con características anómalas (LOLBIN, PowerShell ofuscado - secciones [41.2](#id-41.2-powershell-hunting)/[41.3](#id-41.3-lolbin-hunting-living-off-the-land-binaries)).
* **Why it matters:** suele indicar ejecución de código malicioso disfrazado de actividad legítima del sistema operativo, diseñado para evadir AV tradicional.
* **First 5 checks:** (1) ¿el proceso padre es el esperado para ese binario, o es anómalo (por ejemplo, `winword.exe` lanzando `powershell.exe`)?; (2) ¿la línea de comandos contiene ofuscación (`enc`, Base64, `IEX`)?; (3) ¿el host tiene otra telemetría sospechosa en la misma ventana?; (4) ¿el hash del binario es conocido/firmado?; (5) ¿hubo conexión de red saliente inmediatamente después?
* **Evidence to collect:** línea de comandos completa, hash del binario, proceso padre/hijo (Sysmon EventID 1/3), conexiones de red asociadas.
* **Escalation criteria:** línea de comandos claramente maliciosa (descarga y ejecución en memoria), o conexión saliente a IP/dominio de baja reputación.
* **Containment options:** aislamiento del host vía EDR, terminación del proceso, bloqueo del hash a nivel de EDR/AV.
* **Closure notes:** adjuntar el hash y la línea de comandos al catálogo de IOCs interno si se confirma malicioso.

#### 63.4 Cloud Identity

* **What happened:** actividad anómala sobre una identidad cloud (impossible travel, consent grant, creación de credenciales - secciones [53.2](#id-53.2-impossible-travel), [62.4](#id-62.4-azure-consent-grant-illicit-consent-grant), [62.2](#id-62.2-aws-accesskey-creation)).
* **Why it matters:** las identidades cloud suelen tener alcance amplio (correo, archivos, infraestructura) y el compromiso puede propagarse a múltiples servicios SaaS sin tocar el endpoint.
* **First 5 checks:** (1) ¿el usuario confirma la actividad (viaje, uso de nueva app)?; (2) ¿el dispositivo/IP es reconocido?; (3) ¿hay MFA habilitado y se satisfizo correctamente?; (4) ¿se crearon reglas de reenvío, tokens o claves nuevas tras el evento?; (5) ¿la identidad es privilegiada/VIP (sección [59](#id-59.-asset-and-identity-framework))?
* **Evidence to collect:** logs de sign-in (IP, dispositivo, ubicación, resultado de MFA), aplicaciones con consentimiento OAuth, claves/tokens creados.
* **Escalation criteria:** el usuario no reconoce la actividad, o se detectan acciones posteriores de persistencia (regla de reenvío, nueva credencial).
* **Containment options:** revocar sesiones/tokens, forzar reautenticación con MFA, revocar consentimiento OAuth, desactivar credenciales cloud creadas.
* **Closure notes:** documentar si se confirmó phishing/compromiso previo como causa raíz.

#### 63.5 Data Exfiltration

* **What happened:** volumen o patrón de acceso a datos incompatible con el comportamiento normal del usuario/host (DNS exfiltration, mass download - secciones [53.10](#id-53.10-dns-exfiltration), [62.7](#id-62.7-m365-mass-download-mass-file-access)).
* **Why it matters:** es frecuentemente la etapa final de un ataque (impacto de negocio directo: pérdida de datos, incumplimiento normativo).
* **First 5 checks:** (1) ¿el volumen es consistente con un proceso planificado (backup, migración)?; (2) ¿el destino (dominio DNS, IP externa) es conocido/confiable?; (3) ¿la cuenta/host tuvo actividad sospechosa previa en la cadena (login anómalo, escalada)?; (4) ¿qué tipo de datos están involucrados (sensibilidad, PII, secretos)?; (5) ¿el canal de salida es esperado (HTTP/S corporativo) o encubierto (DNS, puertos no estándar)?
* **Evidence to collect:** volumen de datos (bytes, número de archivos/consultas), destino, usuario/host origen, tipo de contenido accedido.
* **Escalation criteria:** destino no confiable confirmado, o datos sensibles/regulados involucrados.
* **Containment options:** bloqueo del dominio/IP de destino, aislamiento del host, revocación de acceso a los recursos de datos afectados.
* **Closure notes:** notificar a Legal/Compliance si se confirma exfiltración de datos regulados; documentar volumen estimado exfiltrado.

#### 63.6 Privilege Escalation

* **What happened:** una cuenta obtuvo privilegios adicionales fuera de un proceso de gestión de accesos conocido (sección [53.6](#id-53.6-privilege-escalation)).
* **Why it matters:** amplía drásticamente el alcance de un atacante (o de un insider) sobre el entorno, habilitando movimiento lateral y persistencia adicional.
* **First 5 checks:** (1) ¿existe un ticket de cambio asociado a esta modificación?; (2) ¿quién ejecutó el cambio (`Subject_User_Name`) y tiene permisos de IAM legítimos?; (3) ¿el grupo/rol otorgado es crítico (Domain Admins, roles cloud administrativos)?; (4) ¿la cuenta objetivo mostró actividad inmediatamente después usando el nuevo privilegio?; (5) ¿hay otros cambios de privilegios simultáneos (patrón de campaña)?
* **Evidence to collect:** EventCodes 4728/4732/4756 (o equivalente cloud: `AssumeRole`, cambios de rol en Azure AD), usuario que ejecuta el cambio, ticket de cambio si existe.
* **Escalation criteria:** sin ticket de cambio y el grupo/rol es crítico, o la cuenta objetivo ya muestra signos de compromiso previo.
* **Containment options:** revertir la membresía/rol otorgado, suspender la cuenta que ejecutó el cambio si es sospechosa.
* **Closure notes:** confirmar con IAM/gestión de accesos si el cambio se formalizará retroactivamente o se revierte.

#### 63.7 Mailbox Compromise

* **What happened:** indicios de control no autorizado sobre un buzón de correo (regla de reenvío sospechosa, acceso desde ubicación anómala - sección [62.6](#id-62.6-m365-inbox-rule-creacion-de-reglas-de-reenvio-sospechosas)).
* **Why it matters:** el correo es un vector habitual tanto de exfiltración de información como de fraude (Business Email Compromise) contra terceros (proveedores, clientes).
* **First 5 checks:** (1) ¿la regla de reenvío/eliminación apunta a un dominio externo no reconocido?; (2) ¿hubo un login anómalo (impossible travel, IP no habitual) antes de la creación de la regla?; (3) ¿se han enviado correos fraudulentos desde el buzón (por ejemplo, solicitudes de cambio de datos bancarios)?; (4) ¿el usuario reconoce haber creado la regla?; (5) ¿hay otros buzones con el mismo patrón (campaña más amplia)?
* **Evidence to collect:** configuración exacta de la regla, IP/dispositivo del login previo, correos enviados/recibidos relevantes en la ventana sospechosa.
* **Escalation criteria:** regla de reenvío a dominio externo confirmada, o evidencia de correos fraudulentos ya enviados a contactos externos.
* **Containment options:** eliminar la regla de reenvío, revocar sesiones y tokens, forzar rotación de credenciales y MFA.
* **Closure notes:** notificar a los contactos externos si se enviaron correos fraudulentos desde el buzón comprometido; documentar el vector de compromiso inicial si se identifica.

***

### 64. Dashboards recomendados

Los KPIs y validaciones descritos a lo largo de este documento (secciones [48](#id-48.-coverage-engineering), [55](#id-55.-cim-validation), [57](#id-57.-kpis-de-soc)) tienen más valor cuando se visualizan de forma continua. Estos son los dashboards mínimos recomendados para un programa de detección maduro:

<table><thead><tr><th>Dashboard</th><th width="268">Contenido recomendado</th><th width="238">Secciones relacionadas</th></tr></thead><tbody><tr><td><strong>Detection Health Dashboard</strong></td><td>Estado del ciclo de vida de cada detección (Draft/Testing/Production/Monitoring/Tuning/Retirement), owner, fecha de última revisión, volumen de disparos y FPR por detección.</td><td><a href="#id-48.-coverage-engineering">48</a>, <a href="#id-56.-detection-lifecycle">56</a></td></tr><tr><td><strong>CIM Coverage Dashboard</strong></td><td>% de cobertura por Data Model (Authentication, Endpoint, Network Traffic, etc.), estado de aceleración, sourcetypes sin mapear.</td><td><a href="#id-42.-data-models-en-profundidad">42</a>, <a href="#id-47.-cim-mapping-de-campo-original-a-campo-cim">47</a>, <a href="#id-55.-cim-validation">55</a></td></tr><tr><td><strong>SOC KPI Dashboard</strong></td><td>Tendencia de notables/día, FPR por detección (top 10 más ruidosas), MTTD/MTTR por severidad, coverage % por táctica ATT&#x26;CK.</td><td><a href="#id-57.-kpis-de-soc">57</a></td></tr><tr><td><strong>Risk Events Dashboard</strong></td><td>Volumen de risk events por <code>risk_object_type</code>, top entidades por riesgo acumulado, distribución de <code>risk_score</code> por técnica MITRE.</td><td><a href="#id-40.-risk-based-alerting-rba">40</a>, <a href="#id-61.-attack-chains-encadenando-tecnicas-att-and-ck">61</a></td></tr><tr><td><strong>Data Source Health Dashboard</strong></td><td>Volumen de eventos por sourcetype (últimas 24h vs. baseline histórico), fuentes con caída/pico de volumen, latencia de indexación.</td><td><a href="#id-46.-common-soc-data-sources">46</a>, <a href="#id-65.-data-quality-rules">65</a></td></tr></tbody></table>

#### 64.1 Ejemplo de panel: tendencia de volumen por sourcetype (Data Source Health)

```
| tstats count WHERE index=* BY sourcetype _time span=1h
| timechart span=1h sum(count) BY sourcetype limit=10
```

#### 64.2 Ejemplo de panel: FPR por detección (SOC KPI Dashboard)

```
| tstats count FROM datamodel=Notable WHERE index=notable status=closed earliest=-30d@d BY search_name disposition
| eventstats sum(count) AS total_closed BY search_name
| eval fpr=round(count/total_closed,3)
| where disposition="*false_positive*"
| sort - fpr
| table search_name fpr total_closed
```

> Estos paneles son puntos de partida; ajusta los nombres de campo/índice a los que use tu entorno concreto de Enterprise Security (validar contra el checklist de la sección [51](#id-51.-production-readiness-checklist) antes de considerarlos definitivos).

***

### 65. Data Quality Rules

La sección [48](#id-48.-coverage-engineering) mide cobertura de detecciones y la [55](#id-55.-cim-validation) valida el mapeo CIM. Esta sección añade un tercer nivel: **detecciones internas sobre la propia calidad del dato**, para identificar cuándo una fuente se ha roto (deja de llegar, cambia de formato, pierde campos) **antes** de que eso provoque que otras detecciones fallen en silencio.

#### 65.1 Detectar caída de volumen por fuente

```
| tstats count WHERE index=* BY sourcetype _time span=1h
| eventstats avg(count) AS avg_count stdev(count) AS stdev_count BY sourcetype
| where count < (avg_count - 2*stdev_count)
| table _time sourcetype count avg_count
```

Detecta cuándo el volumen de una fuente cae muy por debajo de su comportamiento histórico (caída de más de 2 desviaciones estándar) - síntoma típico de un forwarder caído, un fallo de conectividad, o un cambio de configuración accidental en origen.

#### 65.2 Detectar campos críticos vacíos

```
index=windows sourcetype=WinEventLog:Security earliest=-24h
| where isnull(user) OR user=""
| stats count BY host
| where count > 0
```

Si un campo crítico para las detecciones (`user`, `src_ip`, `dest`) empieza a llegar vacío para un `host`/fuente concreta, es una señal de que el parsing (`props.conf`/`transforms.conf`, sección [54](#id-54.-data-onboarding-y-parsing-pipeline)) se ha roto - por ejemplo, tras una actualización del agente o un cambio de formato de log en el origen - y cualquier detección que dependa de ese campo dejará de funcionar silenciosamente para esos eventos.

#### 65.3 Detectar fuentes nuevas o desaparecidas

```
| metadata type=sourcetypes index=*
| eval last_seen_hours_ago=round((now()-lastTime)/3600,1)
| where last_seen_hours_ago > 24
| table sourcetype last_seen_hours_ago totalCount
```

Lista sourcetypes que llevan más de 24 horas sin recibir eventos - útil como chequeo periódico independiente de cualquier detección concreta, para detectar fuentes que han dejado de enviar datos por completo.

#### 65.4 Checklist de calidad de datos

| Regla                                                 | Qué detecta                                                                    | Frecuencia recomendada      |
| ----------------------------------------------------- | ------------------------------------------------------------------------------ | --------------------------- |
| Caída de volumen (65.1)                               | Forwarder caído, problema de conectividad, cambio de configuración accidental. | Cada hora.                  |
| Campos críticos vacíos (65.2)                         | Parsing roto tras cambio de formato de log o actualización de agente.          | Diario.                     |
| Fuentes sin actividad reciente (65.3)                 | Fuente completamente caída o descomisionada sin actualizar el inventario.      | Diario.                     |
| Validación CIM (sección [55](#id-55.-cim-validation)) | Mapeo CIM incompleto o desactualizado tras onboarding de una fuente nueva.     | Al onboardear + trimestral. |

> Trata estas reglas como **detecciones de primera clase**, no como tareas de infraestructura aparte: dales el mismo tratamiento de lifecycle (sección [56](#id-56.-detection-lifecycle)) y de alerta que a cualquier detección de seguridad - una fuente de datos rota es, en la práctica, un punto ciego de seguridad silencioso.
