Network Security with Zeek (Bro)

Posted by Craig Jorgensen
Craig Jorgensen

Zeek the new Bro

Zeek is the new name for Bro that has been in existence since 1994. In this article, we will review the useful features of Zeek that make it a powerful tool for network analysis and security monitoring. Need a little more familiarity with Zeek? Check out our previous blog:  Bro: Security's Swiss Army Knife.

Three creative business team working on electronic devices in the office

A programming language like Zeek is a very rich toolset in the hands of a security engineer, analyst, or even sys admin. Zeek, when it was known as Bro, had its beginnings long ago, at the University of Berkeley with 20 years of teamwork going into development. Today it has found widespread adoption in industry circles.

Some compare it to snort IDS as it has full programming capabilities and support for several data types and network events. This means that it is a little bit of a networking tool like netcat or tcpdump, but with a language that allows looping, conditions, and much more. So it is like a shell script for security analysis.

For those unfamiliar with Zeek, it is not the easiest to learn, but very important. The main reason being we are used to programming paradigms from the scripting or OO world or functional programming world. But Zeek is entirely event-driven; it takes a different approach as it is single-threaded, and its scoping and code flow works differently from other languages you might have learned in the past.

It is, however, swift and bug-free, used widely, and it comes preloaded with hundreds of frameworks and scripts that you can straight away plugin and start using without much fuss.

Zeek is not entirely a language, as it is tailor-made for network security analysis. Zeek relies upon doing things the right way, and you, as a developer, have to follow its rules to benefit by its built-in techniques and frameworks. It supports data types such as sets, vectors, tables, addresses, and a record data type that you can create a custom data type.

Zeek can function as a protocol analyzer, it can do passive and active network monitoring, and it can also serve as a logger. Logging is one area Zeek has been focusing on, and today with big-data repositories and tools like Splunk, Zeek fits in nicely. You can do stuff similar to Cisco Netflow with Zeek. 


Sample Scripts

In this blog, we shall get our feet wet and write simple scripts to get a feel of the Zeek ecosystem and semantics. In a sequel article, we can drill down into specifics and see how Zeek can do some intelligent processing of network behavior to help us make decisions later.

Let us begin with a rather simple but useful example here:

@load base/protocols/conn                    

@load base/protocols/http

event connection_state_remove(c: connection)


    print c;



This prints the output:

# zeek -b conntrack.zeek -i enp1s0              

listening on enp1s0                                                                        

[id=[orig_h=, orig_p=8443/tcp, resp_h=, resp_p=37546/tcp], orig=[size=0, state=6, num_pkts=1, num_bytes_ip=40, flow_label=0, l2_addr=00:6d:61:58:6d:ff], resp=[size=0, state=0, num_pkts=0, num_bytes_ip=0, flow_label=0, l2_addr=8c:dc:d4:d2:d8:6d], start_time=1588222221.612365, duration=0.0, service={ }, history=R, uid=CpSOpd3oSR4CTgHod9, tunnel=<uninitialized>, vlan=<uninitialized>,inner_vlan=<uninitialized>, successful=T, conn=[ts=1588222221.612365, uid=CpSOpd3oSR4CTgHod9, id=[orig_h=, orig_p=8443/tcp, resp_h=, resp_p=37546/tcp], proto=tcp, service=<uninitialized>, duration=<uninitialized>, orig_bytes=<uninitialized>, resp_bytes=<uninitialized>, conn_state=RSTOS0, local_orig=<uninitialized>, local_resp=<uninitialized>, missed_bytes=0, history=R, orig_pkts=1, orig_ip_bytes=40, resp_pkts=0, resp_ip_bytes=0, tunnel_parents=<uninitialized>], extract_orig=F, extract_resp=F, thresholds=<uninitialized>, http=<uninitialized>, http_state=<uninitialized>]

[id=[orig_h=, orig_p=42198/tcp, resp_h=, resp_p=8443/tcp],orig=[size=0, state=1, num_pkts=1, num_bytes_ip=60, flow_label=0, l2_addr=8c:dc:d4:d2:d8:6d], resp=[size=0, state=6, num_pkts=1, num_bytes_ip=40, flow_label=0, l2_addr=00:6d:61:58:6d:ff], start_time=1588222221.612628, duration=0.245077, service={

*This is a snippet of the entire code, as I removed some lines.

The -b switch stands for quick startup to avoid loading Zeek's full system. And the -i switch stands for reading from the ethernet network interface in my Linux box.

This process also creates a conn.log file which has contents like this:

cat conn.log #separator \x09 #set_separator ,                                                                  

#empty_field (empty) #unset_field - #path conn                                                                       

#open 2020-04-30-10-20-26 #fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service duration orig_bytes resp_bytes conn_state loc

al_orig local_resp missed_bytes history orig_pkts orig_ip_bytes resp_pkts resp_ip_bytes tunnel_parents #types time string addr port addr port enum string interval c

ount count string bool bool count string count count count count set[string] 1588222221.612365 CpSOpd3oSR4CTgHod9 8443

2 37546 tcp - - - - RSTOS0 - - 0 R

1 40 0 0 - 1588222221.612628 CxJkHX3a0jR6HFiIO6 42198

59 8443 tcp - 0.245077 0 0 REJ -

As you can see Zeek, does give you a lot of information that could appear like noise. So it is often essential to use other Linux tools like grep and friends to fish out the lines of interest from what the Zeek scripts log.

Zeek itself has a syntax that invokes events inside a when block. And you can use several data types and read from files like whitelist and blacklist of IP addresses to take action against connections. Zeek serves as a passive network security monitor and an active alerting system when integrated with email alerts and drops connections from untrusted IP addresses. One typical attack, such as an SSH brute force, can easily be detected using the Zeek's summary statistics script, which measures the failed login attempts and triggers a condition once it reaches a user-defined threshold.

Here is one more example of the programming primitives like switch in Zeek:

event zeek_init()                                                                


    local x = 7;

    switch ( x ) 


        case 2, 3:

            # This block executes if any of the case labels match.

            print "case 2, 3";


        case 4:

            print "case 4 and ...";

            # Block ending in the "fallthrough" also execute subsequent case.


        case 7:

            print "7";




            # This block executed if no other case matches.

            print "default case";





This code illustrates the fundamental concept that all our non-event code, which is regular code, must sit inside the zeek_init() event handler, which fires up as soon as the script is loaded and executed.

It is a regular switch condition like we see in other common programming languages. Zeek easily accomplishes difficult tasks by pushing events into a single thread queue and executing it in turns.

In the next few articles, we will discuss how Zeek detects network intrusion and can do reconnaissance work.

Did you enjoy this content? Follow our linkedin page!


Looking for similar content?

Craig Jorgensen

Written by Craig Jorgensen

I'm a recent graduate from South Dakota State University with a degree in Computer Science. For me programming is both a passion and an occupation, but have been broadening my horizons as Customer Success Manager with Query.AI, where we are using Natural Language Processing to allow users to “talk to your data”.