Let’s Get Scripting!
Before we dive into type-specific sections and start writing our first actual script, let’s make sure we understand the practical workflow of Zeek script development.
Setting Up Your Development Environment
Choosing an Editor
Zeek scripts are plain text files with a .zeek
extension (older scripts may use .bro
). You can write them in any text editor, but some offer better support than others:
Visual Studio Code (Recommended for beginners)
- Install the “Zeek Language” extension for syntax highlighting
- Provides reasonable auto-completion and error detection
- Lightweight and cross-platform
Vim/Neovim (Recommended for terminal users)
- Install zeek.vim syntax plugin from: https://github.com/zeek/vim-zeek
- Fast, powerful, works over SSH
- Place syntax file in
~/.vim/syntax/zeek.vim
- Add to
.vimrc
:au BufRead,BufNewFile *.zeek set filetype=zeek
Emacs
- Zeek mode available, search for “zeek-mode”
- Powerful but steeper learning curve
Sublime Text / Atom
- Various community plugins available
- Check package managers for “Zeek” or “Bro”
Basic syntax highlighting is crucial - it helps you spot typos, makes code more readable, and catches obvious errors before you even load the script.
Script Linting and Validation
Before loading a script into Zeek, you can check it for syntax errors:
# Check script syntax without running it
zeek -a your-script.zeek
The -a
flag performs a full parse without execution. This catches syntax errors, type mismatches, and undefined variables before you load the script into your running Zeek instance.
Common errors you’ll catch:
- Missing semicolons
- Undeclared variables
- Type mismatches (adding a string to a count, etc.)
- Malformed regular expressions
- Incorrect function signatures
Get in the habit of running zeek -a
before deploying any script. It saves time and prevents broken deployments.
Where Scripts Live: The Zeek Directory Structure
Understanding where scripts go and how Zeek finds them is essential:
/opt/zeek/ # Main Zeek installation
├── bin/ # Zeek executables (zeek, zeekctl)
├── share/zeek/ # Zeek's script library
│ ├── base/ # Core scripts (protocols, frameworks)
│ ├── policy/ # Optional detection scripts
│ └── site/ # YOUR CUSTOM SCRIPTS GO HERE
│ ├── local.zeek # Main site configuration
│ └── custom/ # Organize your scripts here
└── logs/ # Log output (when running)
Key principle: Put your custom scripts in /opt/zeek/share/zeek/site/
or subdirectories within it.
This keeps your code separate from Zeek’s built-in scripts, making updates easier and organization clearer.
Creating Your First Script
Let’s create a simple “hello world” script to verify the workflow:
# Navigate to the site directory
cd /opt/zeek/share/zeek/site/
# Create a directory for your custom scripts (optional but recommended)
sudo mkdir -p custom
# Create your first script
sudo nano custom/hello.zeek
In the editor, write:
# hello.zeek - Your first Zeek script
# This script proves your workflow is working
event zeek_init()
{
print "Hello from Zeek! Script loaded successfully.";
print fmt("Zeek started at: %s", network_time());
}
event zeek_done()
{
print "Zeek is shutting down. Goodbye!";
}
Understanding this script:
zeek_init()
fires once when Zeek starts - perfect for initialization and testingzeek_done()
fires once when Zeek shuts downprint
outputs to stdout (visible in foreground mode) or logsfmt()
formats strings like printf
Save the file (Ctrl+O, Enter, Ctrl+X in nano).
Let’s now also test to ensure our script works:
zeek -a custom/hello.zeek
In this case - no output is good. If we had some error - try it yourself by introducing an intentional mangle - you’ll see output related to it:
zeek -a custom/hello.zeek
error in ./custom/hello.zeek, line 13: syntax error, at or near "}"
Loading Scripts: The local.zeek Method
As we touched on in Module 01, local.zeek
file is our site configuration hub. It’s loaded automatically by Zeek and is where you specify which scripts to load, set configuration options, and customize behaviour.
Edit local.zeek
:
sudo nano /opt/zeek/share/zeek/site/local.zeek
As we saw before, already has content (network definitions, loaded scripts, etc.). It would be worth spending some time at some point reviewing all the content to make sure you understand what everything does, and in general how the script is put together and operates.
For now however, let’s import our new script:
# Load our custom hello world script
@load ./custom/hello.zeek
The @load
directive tells Zeek to load and execute the specified script. The path ./custom/hello.zeek
is relative to the site/
directory.
Alternative loading methods:
# Load by absolute path (less common)
@load /opt/zeek/share/zeek/site/custom/hello.zeek
# Load entire directory (loads all .zeek files in it)
@load ./custom/
# Load with namespace (for organization)
@load custom/detection/scanning
Testing Your Script
Now let’s verify everything works:
Method 1: Test in Foreground
Use this to run an instance of Zeek to test it, which is recommended for R&D purposes.
# Run Zeek in foreground on a network interface
sudo zeek -i eth0 local.zeek
# Or run on a PCAP file for testing
sudo zeek -r /path/to/capture.pcap local.zeek
Once it’s up and running you can hit Ctrl + C to stop. You should immediately see:
sudo zeek -i eth0 local.zeek
listening on eth0
Hello from Zeek! Script loaded successfully.
Zeek started at: 0.0
1760440271.105128 115 packets received on interface eth0, 0 (0.00%) dropped, 0 (0.00%) not processed
...
Zeek is shutting down. Goodbye!
We can see both the output when the script starts, and the output when Zeek shuts down.
Method 2: Deploy with ZeekControl (Production method)
For running Zeek as a service (which you learned in the previous section), there’s an important difference: print
statements don’t appear when Zeek runs as a daemon.
This is of course because daemons run in the background, so don’t have the ability to output to the active terminal.
So instead, we need a script that writes to Zeek’s logging system. Let’s create a daemon-friendly version:
# Create a new script for daemon mode
sudo nano /opt/zeek/share/zeek/site/custom/hello-daemon.zeek
# hello-daemon.zeek
module HelloDaemon;
export {
redef enum Log::ID += { LOG };
type Info: record {
ts: time &log;
message: string &log;
};
}
event zeek_init()
{
Log::create_stream(HelloDaemon::LOG, [$columns=Info, $path="hello"]);
Log::write(HelloDaemon::LOG, [
$ts=network_time(),
$message="Hello from Zeek daemon! Script loaded successfully."
]);
}
event zeek_done()
{
Log::write(HelloDaemon::LOG, [
$ts=network_time(),
$message="Zeek daemon is shutting down. Goodbye!"
]);
}
Test to make sure there are no errors:
Get in the habit of doing this and develop the muscle memory - ALWAYS run zeek -a
against a newly minted script.
zeek -a custom/hello-daemon.zeek
Load it in local.zeek:
sudo nano /opt/zeek/share/zeek/site/local.zeek
Comment out the old one and add the new one at the bottom:
# Custom script to test
# @load ./custom/hello.zeek
@load ./custom/hello-daemon.zeek
Now deploy with zeekctl:
# Check configuration is valid
sudo zeekctl check
# If no errors, install the new configuration
sudo zeekctl install
# Restart Zeek to load new scripts
sudo zeekctl restart
# Check that Zeek is running
sudo zeekctl status
Verify the script loaded and is working:
cat /opt/zeek/logs/current/hello.log
You should see output like:
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path hello
#open 2024-10-14-07-13-05
#fields ts message
#types time string
1728891185.123456 Hello from Zeek daemon! Script loaded successfully.
To see the shutdown message:
Stop Zeek and check the log, remember Zeek will archive the log from the current
directory to the timestamped directory, since mine will be different than yours you cannot blindly C+P this command - find your specific log.
sudo zeekctl stop
# using zcat since files are gzipped
zcat /opt/zeek/logs/2025-10-14/hello.07\:27\:23-07\:27\:24.log.gz
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path hello
#open 2025-10-14-07-27-23
#fields ts message
#types time string
1760441243.306404 Zeek daemon is shutting down. Goodbye!
#close 2025-10-14-07-27-23
And now if you’d like, just remember to start Zeek again using sudo zeekctl start
.
Development Workflow: The Iteration Cycle
Here’s a quick recap of the overall workflow we’ll be using constantly:
1. Write/Edit Script
sudo nano /opt/zeek/share/zeek/site/custom/your-script.zeek
2. Check Syntax
cd /opt/zeek/share/zeek/site/
zeek -a custom/your-script.zeek
3. Test in Foreground (Fast iteration)
# Run on live interface
sudo zeek -i eth0 local.zeek
# Or run on test PCAP
sudo zeek -r test.pcap local.zeek
4. Generate Test Traffic
# In another terminal, trigger your detection
curl http://example.com
# or whatever triggers your script
5. Observe Output
- Watch stdout in the terminal where Zeek is running
- Check logs in current directory:
ls *.log
6. Fix/Improve Script - Go back to step 1
7. Deploy to Production (when satisfied)
sudo zeekctl check
sudo zeekctl install
sudo zeekctl restart
Common Pitfalls and Solutions
Problem: “error loading script” or “file not found”
- Check the path in your
@load
directive - Verify the file exists:
ls -la /opt/zeek/share/zeek/site/custom/
- Make sure path is relative to
site/
directory - Check for typos in filename
Problem: Script loads but doesn’t seem to do anything
- Add
print
statements to verify script is executing - Check you’re monitoring the right interface:
ip addr
to list interfaces - Verify traffic is flowing:
sudo tcpdump -i eth0 -c 10
- For event handlers, make sure events are actually firing
Problem: Syntax error messages
- Read the error carefully - Zeek tells you line number and what’s wrong
- Common issues: missing semicolons, incorrect types, typos in variable names
- Use
zeek -a
before loading to catch errors early
Problem: Changes don’t appear after editing
- With zeekctl: Did you run
zeekctl install
andzeekctl restart
? - With foreground: Did you stop and restart Zeek?
- Zeek doesn’t auto-reload - you must restart after changes
Best Practices for Script Development
1. Start Simple
- Write minimal code first (just print statements)
- Verify it loads and runs
- Add complexity incrementally
2. Use Descriptive Names
# Good
local suspicious_connection_count: count = 0;
# Bad
local x: count = 0;
3. Comment Your Code
# Track failed SSH attempts per IP
# Alert when threshold exceeded
global ssh_failures: table[addr] of count;
4. Test with Known Data
- Keep test PCAP files for common scenarios
- Generate controlled test traffic
- Verify detections fire when they should (and don’t when they shouldn’t)
5. Log Your Detections
# Don't just print - write to notice framework or custom logs
# We'll cover this more later, but for now:
print fmt("DETECTION: Suspicious activity from %s", ip);
6. Organize Your Scripts
site/
├── local.zeek # Main config
└── custom/
├── scanning/ # Scan detection scripts
├── bruteforce/ # Brute force detection
├── exfiltration/ # Data exfil detection
└── util/ # Helper functions
7. Version Control Your Scripts
cd /opt/zeek/share/zeek/site/custom/
git init
git add .
git commit -m "Initial detection scripts"
This lets you track changes, roll back mistakes, and collaborate.
Verifying Your Setup
Before moving to the type-specific exercises, verify your environment:
# 1. Check Zeek is installed and version
zeek --version
# 2. Check you can write to site directory
sudo touch /opt/zeek/share/zeek/site/custom/test.txt
sudo rm /opt/zeek/share/zeek/site/custom/test.txt
# 3. Verify your hello.zeek loads without errors
cd /opt/zeek/share/zeek/site/
zeek -a custom/hello.zeek
# 4. Test it runs
sudo zeek -i eth0 local.zeek
# Press Ctrl+C after seeing hello message
# 5. Check zeekctl works
sudo zeekctl status
If all of these work, you’re ready to start building real detections!
LET’S DO IT!