This post serves as an introduction to some of the pitfalls I had to learn about whilst writing scripts. Hopefully, they help you avoid the same pitfalls. In some of the below example code snippets, bold font is used to emphasize a particular pitfall. If you’d like to execute the included code samples, you can run (most of) them on try.zeek.org.
These 7 tips were inspired by Aashish Sharma’s 2018 Brocon presentation, Bro scripts – 101 to 595 in 45 mins. Thanks go to Aashish for his presentation and feedback while drafting this blog.
1. Return from events (or break from hooks) ASAP
Zeek’s event engine raises scriptland events as Zeek processes connections. Avoiding a few additional cycles in an event’s body can have a big impact depending on the particular event as well as the traffic volume Zeek is processing. Hooks are a type which operate somewhere between functions and events. Exiting from hooks can be done both with return and break statements.
Do:
event example_event(s1: string) { if (|s1| > 2) { return; } local s2: string = "string2"; print s1, s2; }
Don’t:
event example_event(s1: string) { local s2: string = "string2"; if (|s1| > 2) { return; } print s1, s2; }
2. Create and use your own namespace
Be careful exporting to existing namespaces. It’s like running with scissors, exciting but dangerous. A general rule is to not use existing module names. Module namespaces can even be nested!
Do:
module MyModule; export { global my_value: string = "hello!"; } print MyModule::my_value;
Don’t:
module HTTP; export { global my_cool_http_value: string = "cool!"; } print HTTP::my_cool_http_value;
3. Be explicit
I’ve found being explicit rarely hurts while being implicit makes debugging difficult. The following generates an unknown identifier error. This is because the global variable “s” is defined in namespace “Foo” but not in namespace “Bar”. This holds true for all variables in export statements, including functions and events.
Do:
module Foo; export { global s: string = "sssssss"; } event zeek_init() { print s; } module Bar; event zeek_init() { print Foo::s; }
Don’t:
module Foo; export { global s: string = "sssssss"; } event zeek_init() { print s; } module Bar; event zeek_init() { print s; }
4. Heed scoping
The module declaration is one of the biggest influencers of scope. All variables in Zeek are function scoped or global (in namespace GLOBAL), if not specified. The below will generate a type clash error because “c” is first treated as a string and then as an int. As a colleague put it, “this is an interesting case of a language being loose with scope, but not with types”. You don’t have to explicitly declare variables used in for loops. However, revisit #3.
Do:
local str: string = "hello"; local s: set[int] = {1, 2, 3, 4, -99}; for (c in str) { print c; } for (i in s) { print i; } print c;
Don’t:
local str: string = "hello"; local s: set[int] = {1, 2, 3, 4, -99}; for (c in str) { print c; } for (c in s) { print c; } print c;
5. Bifs are your best friend
Eventually you’ll want to extend the language to do something it doesn’t already. Many times, someone has already solved your problem as a bif (or built-in function). Since bifs need to be compiled, they can be found in the src/ and not the scripts/ directory. The docs are also a great way to read through existing bifs. For example, this is the description for sub_bytes. Be sure to use bifs before trying to implement logic in script.
Creating your own bif is also pretty simple. The bif syntax is simply a Zeek function declaration with a C++ body. Again, see existing bifs for examples of the things bifs can provide. If you’re truly brave, have a look at the bif compiler.
Do:
print sub_bytes("Hello World!", 5, 8);
Don’t:
local n: count = 1; local s: string = ""; for (char in "Hello World!") { if ((n >= 5) && (n <= 5+8)) { s += char; } n += 1; } print s;
6. Appending is different than defining
Zeek has some redefinable variables used for controlling its behavior. These behave like global configuration settings for Zeek. A good file to peruse, to understand the types of global variables which exist in Zeek, is init-bare.zeek.
Tangentially related to redefining global constants is the relatively new option type used by the Configuration Framework. Instead of behaving “like global configuration settings for Zeek”, they are global configuration settings for Zeek.
Do:
const port_list: table[port] of string &redef; redef port_list += { [6666/tcp] = "IRC"}; redef port_list += { [80/tcp] = "WWW" };
Don’t:
const port_list: table[port] of string &redef; redef port_list += { [6666/tcp] = "IRC"}; redef port_list = { [80/tcp] = "WWW" };
7. The Zeek Universe is vast (like, Tolkien-vast)
BTest, Binpac, Spicy, Zeek, zeek-cut, CMake… plugins, packages, frameworks, analyzers… there are a bunch of components and jargon to get stuck on. If you’re struggling, the community is eager to help. Reach out!
Do:
- Contact the Zeek mailing lists.
- Join the Zeek Slack workspace.
- Try the walk-throughs on try.zeek.org.
Don’t:
- Stop Believin’.