Zeek Package for Log Filter
Enables plugins to write fine-grained policy for log filtering, modification, and path customization.
Quick Start
If you already have Zeek and zkg
installed, simply run:
zkg install https://github.com/esnet-security/logfilter
If this is being installed on a cluster, install the package on the manager, then deploy it via:
zeekctl deploy
Updating and Unloading
We use SemVer for versioning. For the versions
available, see the tags on this repository. You can
pass an additional argument to the install
command with the desired
version.
To upgrade to the latest version run:
zkg upgrade logfilter
You can modify the above command by replacing upgrade
with:
unload
, to configure Zeek to not load the package on startup.load
, to configure Zeek to load the package on startup (default after an install).remove
, to delete the package from the system.
If you're operating in a cluster, after performing any of the above changes, you'll need to re-run zeekctl deploy
.
Installation
This is a package designed to run with the Zeek Network Security Monitor. First, get Zeek. We strive to support both the current feature and LTS releases.
The recommended installation method is via the Zeek package manager, zkg. Follow the Quickstart guide.
To have Zeek load packages managed by zkg
, ensure that @load packages
is being loaded by Zeek.
Writing Filters
- Filtering
Let's say that we only want our
ssh.log
file to have connections where the responder's port is 22. .. code-block:: zeek @ifdef ( SSH::Info ) hook pred_hook(stream: Log::ID, filter_name: string, rec: any) { if ( stream != SSH::LOG || filter_name != "default" ) return; local r = rec as SSH::Info; if ( r$id$resp_p != 22/tcp ) break; } @endif To write a filter, we handle thepred_hook
hook. Whenever a script callsLog::write
, the hook fires. If any hook handler execution results in abreak
, the log message is not written. For more information about hooks, see: https://docs.zeek.org/en/current/script-reference/types.html#type-hook Becausepred_hook
is used for all log files, we need to take care to make sure we're handling the right log messages. Wrapping the filter in an@ifdef
directive will prevent syntax errors if the base SSH scripts aren't loaded, for some reason. Using@ifdef
is preferred, since if we were to load the scripts ourselves, we'd remove the user's ability to not load those scripts. Line 4 checks that the message is from the SSH log, and the default filter. The logging framework supports additional filters (see: https://docs.zeek.org/en/current/frameworks/logging.html ), this check just ensures that we're only modifying the behavior of the defaultssh.log
file. Line 7 converts our info record fromany
type to anSSH::Info
record, which allows us to access the fields therein. Finally, we check the responder's port, and break out of the hook if it's not TCP 22. The break causes the message to not be logged. - Redirecting
Next, let's say that instead of simply filtering what gets logged, we want to log messages to two different logs:
ssh.log
andssh_nonstandard_port.log
. First, we create a new log stream: .. code-block:: zeek module LogFilter; @ifdef ( SSH::Info ) export { redef enum Log::ID += { SshNonStdPort_LOG }; } event LogFilter::initialized() { Log::create_stream(SshNonStdPort_LOG, [$columns=SSH::Info, $path="ssh_nonstandard_port"]); } @endif Much of this should look familiar from the Zeek logging framework documentation (https://docs.zeek.org/en/current/frameworks/logging.html#add-a-new-log-file ). The difference is that we create the log in theLogFilter::initialized
event, rather than inzeek_init
. Once the Log Filter has activated and attached itself to all of the logs, this event fires. This provides an easy way to add a new log, without worrying about the log filter attaching itself multiple times, etc. Our hook handler also looks familiar: .. code-block:: zeek @ifdef ( SSH::Info ) hook pred_hook(stream: Log::ID, filter_name: string, rec: any) { if ( stream != SSH::LOG || filter_name != "default" ) return; local r = rec as SSH::Info; if ( r$id$resp_p != 22/tcp ) { Log::write(SshNonStdPort_LOG, r); break; } } @endif The only difference is in line 11, where we write the line to our new log file before breaking out of the hook. - Copying
If in our previous example we omitted the break in line 12, all log lines would be written to
ssh.log
, and only the ones where the server wasn't running on port 22 would be written to our new log. - Modifying Log lines can even be modified before they're written to the log files. Let's say that instead of version 2, one of our servers actually runs SSH version 9000, and we want to set the field appropriately: .. code-block:: zeek @ifdef ( SSH::Info ) hook pred_hook(stream: Log::ID, filter_name: string, rec: any) { if ( stream != SSH::LOG || filter_name != "default" ) return; local r = rec as SSH::Info; if ( r$id$resp_h == 192.168.1.22 ) r$version = 9000; } @endif Note that when modifying log messages like this, we're only modifying them at the very last second before they get written out. Any policy scripts have already inspected this record.
Contributing
Contributions are welcome! The easiest way to give back is to comment on issues that are important to you -- even a quick reaction (thumbs-up/heart/thumbs-down) would help us prioritize issues.
There's a more in-depth contribution guide which lays out some ways that anyone can help.
Package Template
This package was created with a template, using Cruft. A CI job checks for updates to the template. To update the package, simply run:
pip install -U cruft
cruft update
License
This project is licensed under the BSD license. See the LICENSE file for details.