HomePočetnaAboutO namaServicesUslugeISO 42001ISO 42001TeamTimIndustriesIndustrijeProjectsProjektiBlogBlogGet in TouchKontakt

Mastering Zeek Scripting - Network Security Telemetry at ScaleOvladavanje Zeek skriptama - sigurnosna telemetrija mreže u velikom obimu

Network security teams drowning in packet captures need more than storage - they need deterministic, high-speed analysis that turns raw traffic into structured telemetry. That is what Zeek (formerly Bro) was built for: a specialized, Turing-complete language tailored specifically for network analysis and security telemetry generation.

Unlike signature-based intrusion detection systems (IDS), Zeek decodes network protocols natively and leaves the final policy execution entirely to custom user scripts for threat classification and response.


Core capabilities

  • Event-driven: Execution is driven entirely by internal triggers mapped directly to network occurrences - connection startup, HTTP payload detection, SSL handshakes, and hundreds more.
  • State management: Zeek offers globally maintained variables, tables, and sets to systematically track malicious hosts across long timespans without disk storage latency.
  • Policy neutral: The parser decodes protocols natively, but leaves threat classification and response entirely to your scripts.

Native networking types

Unlike standard programming languages that store IP addresses as text strings, Zeek provides native addr (IP addresses) and subnet (CIDR ranges) types.

  • No more text strings: Native typing allows precise comparison operations directly within conditions, avoiding custom regex or string parsing libraries completely.
  • Unified IPv4 and IPv6: Both address types are compiled identically under the hood. When evaluating whether an IP is in a subnet, IPv4 and IPv6 comparison flows are handled natively without script-level logic forks.

Under-the-hood performance: O(1) radix tree lookups

When executing IP checks inside busy network pipes, checking lists sequentially causes severe performance bottlenecks.

Zeek dynamically builds radix trees (Patricia tries) behind the scenes for all declared subnets. Because of this, IP matching complexity remains constant O(1) relative to target size, safeguarding multi-gigabit throughput rates.


Anatomy of a basic script

Here is a simple state-tracking script that shows how these elements come together:

# Global tracker configuration
global my_count = 0;

event new_connection(c: connection)
{
    ++my_count;
    if ( my_count <= 10 )
    {
        print fmt("Conn %s started", c$uid);
    }
}

event zeek_done()
{
    print fmt("Saw %d total connections", my_count);
}

Key structural blocks

  1. Globals: Defined at the top level to store cross-connection state across multiple executions.
  2. Event handlers: Multiple instances can coexist. They never execute line-by-line sequentially; instead, they run asynchronously when called by the engine.
  3. Record traversal: Elements like c$uid are fields belonging to the connection metadata record structured inside Zeek's C++ core.

Real-world security auditing: the AppProfiler script

By capturing metadata on active IP associations in memory via sets, Zeek acts as an instantaneous asset discovery engine. You can split internal networks from external targets instantly, preventing the need to ingest massive raw telemetry logs into downstream databases for post-processing classification.

Here is a practical script (app_profiler.zeek) designed to audit local traffic, detect potential scanners, and alert on known Command and Control (C2) servers:

module AppProfiler;

export {
    # 1. Define your internal subnet boundary
    const local_zones: set[subnet] = {
        172.16.10.0/24
    };

    # 2. Intel set: simulated known malicious C2 IPs
    const threat_intel_ips: set[addr] = {
        8.8.4.4,       # Test placeholder
        45.33.22.11,
        185.220.101.5
    };

    # 3. Exfiltration threshold: alert if a single connection exceeds 50 MB
    const exfil_threshold_bytes: count = 50 * 1024 * 1024;
    const scanner_threshold: count = 30;
}

# Maps a source IP to the set of unique target IPs it talked to
global scanner_tracker: table[addr] of set[addr];

event new_connection(c: connection)
{
    local src = c$id$orig_h;
    local dst = c$id$resp_h;

    # USE CASE 1: Threat intelligence match
    if ( dst in threat_intel_ips )
    {
        print fmt("[CRITICAL ALERT] Internal node %s initiated connection to known C2 intel IP: %s on port %s",
                  src, dst, c$id$resp_p);
    }

    # USE CASE 2: Scanner detection (internal host mapping many external endpoints)
    if ( src in local_zones && dst !in local_zones )
    {
        if ( src !in scanner_tracker )
            scanner_tracker[src] = set();

        if ( dst !in scanner_tracker[src] )
        {
            add scanner_tracker[src][dst];

            if ( |scanner_tracker[src]| >= scanner_threshold )
            {
                print fmt("[SUSPICIOUS BEHAVIOR] Possible scanner detected! Host %s has mapped %d unique endpoints.",
                          src, |scanner_tracker[src]|);
            }
        }
    }
}

event connection_state_remove(c: connection)
{
    local total = c$orig$size + c$resp$size;

    # USE CASE 3: Large outbound transfer from an internal host
    if ( total >= exfil_threshold_bytes && c$id$orig_h in local_zones )
    {
        print fmt("[DATA EXFIL ALERT] Large transfer (%d bytes) from internal host %s to %s",
                  total, c$id$orig_h, c$id$resp_h);
    }
}

Running it in Docker

You can execute and test your Zeek scripts immediately in containerized environments:

sudo docker run --rm -it --net=host -v $(pwd):/scripts zeek/zeek:lts zeek -i br0.10 /scripts/app_profiler.zeek

Example output showing scanner detection:

listening on br0.10
[SUSPICIOUS BEHAVIOR] Possible scanner detected! Host 172.16.10.2 has mapped 31 unique endpoints.
[SUSPICIOUS BEHAVIOR] Possible scanner detected! Host 172.16.10.2 has mapped 32 unique endpoints.

Want to test online? Visit try.zeek.org or explore the code on github.com/zeek.


Key takeaways

  • Zeek provides deterministic, speed-optimized analysis (via radix trees) to keep up with gigabit network traffic and dynamically filter out bad actors.
  • Event-driven scripts let you encode policy without touching the core engine - decode once, classify many times.
  • In-memory state (tables, sets, and counters) turns live traffic into asset discovery and threat alerts without waiting on a SIEM pipeline.

Our cybersecurity practice uses Zeek-style telemetry pipelines alongside custom detection logic for clients who need visibility without drowning in raw PCAPs. Get in touch if you want help designing network security monitoring for your environment.

Timovima za mrežnu sigurnost koji tonu u snimcima paketa ne treba samo skladištenje - treba im deterministička, brza analiza koja sirov saobraćaj pretvara u strukturiranu telemetriju. Upravo za to je napravljen Zeek (bivši Bro): specijalizovani, Turing-kompletan jezik prilagođen analizi mreže i generisanju sigurnosne telemetrije.

Za razliku od IDS sistema zasnovanih na potpisima, Zeek nativno dekodira mrežne protokole, a klasifikaciju prijetnji i odgovor u potpunosti prepušta prilagođenim korisničkim skriptama.


Osnovne mogućnosti

  • Izvršavanje vođeno događajima (event-driven): Pokreću ga interni okidači mapirani direktno na mrežne pojave - otvaranje konekcije, HTTP payload, SSL handshake i stotine drugih.
  • Upravljanje stanjem: Zeek nudi globalne varijable, tabele i setove za sistematsko praćenje zlonamjernih hostova kroz duže periode, bez kašnjenja zbog upisa na disk.
  • Neutralnost prema politici: Parser dekodira protokole nativno, ali klasifikaciju prijetnji i odgovor prepušta vašim skriptama.

Nativni mrežni tipovi

Za razliku od jezika koji IP adrese drže kao tekstualne stringove, Zeek nudi nativne tipove addr (IP adrese) i subnet (CIDR opsege).

  • Bez tekstualnih stringova: Nativno tipiziranje omogućava precizna poređenja direktno u uslovima, bez regex-a ili parsiranja stringova.
  • Ujedinjeni IPv4 i IPv6: Oba tipa adresa se kompajliraju identično. Provjera da li je IP u subnetu radi nativno, bez grananja logike u skripti.

Performanse ispod haube: O(1) radix tree pretrage

Kod provjera IP adresa na opterećenim mrežnim vezama, sekvencijalno prolaženje kroz liste stvara ozbiljna uska grla.

Zeek dinamički gradi radix stabla (Patricia trie) iza kulisa za sve deklarisane subnete. Zbog toga složenost podudaranja IP adresa ostaje konstantna O(1) u odnosu na veličinu liste, što čuva multi-gigabit propusnost.


Anatomija osnovne skripte

Jednostavna skripta za praćenje stanja pokazuje kako se elementi spajaju:

# Global tracker configuration
global my_count = 0;

event new_connection(c: connection)
{
    ++my_count;
    if ( my_count <= 10 )
    {
        print fmt("Conn %s started", c$uid);
    }
}

event zeek_done()
{
    print fmt("Saw %d total connections", my_count);
}

Ključni strukturni blokovi

  1. Globalne varijable: Definisane na vrhu za stanje koje prelazi više konekcija.
  2. Rukovaoci događajima (event handlers): Može ih koegzistirati više. Ne izvršavaju se liniju po liniju sekvencijalno, već asinhrono kada ih motor pozove.
  3. Pristup poljima zapisa: Elementi poput c$uid su polja metapodataka konekcije strukturirana u Zeek-ovom C++ jezgru.

Sigurnosni audit u praksi: AppProfiler skripta

Snimajući metapodatke o aktivnim IP asocijacijama u memoriji preko setova, Zeek djeluje kao motor za trenutno otkrivanje resursa. Interne mreže možete odvojiti od eksternih ciljeva odmah, bez uvlačenja ogromnih sirovih logova u bazu za naknadnu klasifikaciju.

Praktična skripta (app_profiler.zeek) za audit lokalnog saobraćaja, detekciju skenera i upozorenja na poznate Command and Control (C2) servere:

module AppProfiler;

export {
    # 1. Define your internal subnet boundary
    const local_zones: set[subnet] = {
        172.16.10.0/24
    };

    # 2. Intel set: simulated known malicious C2 IPs
    const threat_intel_ips: set[addr] = {
        8.8.4.4,       # Test placeholder
        45.33.22.11,
        185.220.101.5
    };

    # 3. Exfiltration threshold: alert if a single connection exceeds 50 MB
    const exfil_threshold_bytes: count = 50 * 1024 * 1024;
    const scanner_threshold: count = 30;
}

# Maps a source IP to the set of unique target IPs it talked to
global scanner_tracker: table[addr] of set[addr];

event new_connection(c: connection)
{
    local src = c$id$orig_h;
    local dst = c$id$resp_h;

    # USE CASE 1: Threat intelligence match
    if ( dst in threat_intel_ips )
    {
        print fmt("[CRITICAL ALERT] Internal node %s initiated connection to known C2 intel IP: %s on port %s",
                  src, dst, c$id$resp_p);
    }

    # USE CASE 2: Scanner detection (internal host mapping many external endpoints)
    if ( src in local_zones && dst !in local_zones )
    {
        if ( src !in scanner_tracker )
            scanner_tracker[src] = set();

        if ( dst !in scanner_tracker[src] )
        {
            add scanner_tracker[src][dst];

            if ( |scanner_tracker[src]| >= scanner_threshold )
            {
                print fmt("[SUSPICIOUS BEHAVIOR] Possible scanner detected! Host %s has mapped %d unique endpoints.",
                          src, |scanner_tracker[src]|);
            }
        }
    }
}

event connection_state_remove(c: connection)
{
    local total = c$orig$size + c$resp$size;

    # USE CASE 3: Large outbound transfer from an internal host
    if ( total >= exfil_threshold_bytes && c$id$orig_h in local_zones )
    {
        print fmt("[DATA EXFIL ALERT] Large transfer (%d bytes) from internal host %s to %s",
                  total, c$id$orig_h, c$id$resp_h);
    }
}

Pokretanje u Dockeru

Zeek skripte možete odmah testirati u kontejnerizovanom okruženju:

sudo docker run --rm -it --net=host -v $(pwd):/scripts zeek/zeek:lts zeek -i br0.10 /scripts/app_profiler.zeek

Primjer izlaza sa detekcijom skenera:

listening on br0.10
[SUSPICIOUS BEHAVIOR] Possible scanner detected! Host 172.16.10.2 has mapped 31 unique endpoints.
[SUSPICIOUS BEHAVIOR] Possible scanner detected! Host 172.16.10.2 has mapped 32 unique endpoints.

Želite testirati online? Posjetite try.zeek.org ili pogledajte kod na github.com/zeek.


Ključni zaključci

  • Zeek daje determinističku, brzu analizu (preko radix stabala) koja drži korak sa gigabitnim saobraćajem i dinamički filtrira loše aktere.
  • Skripte vođene događajima omogućavaju politiku bez diranja jezgra - dekodiraj jednom, klasifikuj mnogo puta.
  • Stanje u memoriji (tabele, setovi, brojači) pretvara saobraćaj u otkrivanje resursa i upozorenja bez čekanja na SIEM pipeline.

Naša praksa kibernetičke sigurnosti koristi telemetrijske pipeline-ove u Zeek stilu uz prilagođenu detekciju za klijente kojima treba vidljivost bez tonjenja u sirovim PCAP-ovima. Javite nam se ako vam treba pomoć oko dizajna mrežnog nadzora.