JavaScript¶
New in version 6.0.
Note
Link to external ZeekJS documentation.
Note
The JavaScript integration does not provide Zeek’s typical backwards compatibility guarantees at this point. The plugin itself is at semantic version 0.9.1 at the time of writing meaning the API is not stable. That said, we’ll avoid unnecessary breakage.
Preamble¶
In the scope of integrating with external systems, Zeek can be extended by
implementing C++ plugins or using the system
function to call external programs from Zeek scripts. The Input Framework
can be leveraged for data ingestion (with raw reader
reader providing flexibility to consume input from external programs as events).
The Broker Communication Framework is popular for exchanging events between
Zeek and an external program using WebSockets.
The external program sometimes solely acts as a proxy between Zeek and another
external system.
JavaScript integration adds to the above by enabling Zeek to load JavaScript code directly, thereby allowing developers to use its rich ecosystem of built-in and third-party libraries directly within Zeek.
If you previously wanted to start a HTTP server within Zeek, record Zeek
event data on-the-fly to a Redis database, got scared at looking at
ActiveHTTP
’s implementation (or annoyed that it eats all newlines
in HTTP responses), you may want to give JavaScript a go!
Built-in Plugin¶
The external ZeekJS plugin is included with Zeek as an optional built-in plugin. When Node.js development headers and libraries are found when building Zeek from source, the plugin is automatically included.
If Node.js is installed in a non-standard location, -D NODEJS_ROOT_DIR
has
to be provided to ./configure
.
Assuming an installation of Node.js in /opt/node-19.8
, the command to
use is as follows. Discovered headers and libraries will be reported in the
output.
On Linux distributions providing Node.js development packages
(Ubuntu 22.10, Fedora, Debian bookworm) the extra -D NODEJS_ROOT_DIR
is not required.
$ ./configure -D NODEJS_ROOT_DIR:string=/opt/node-19.8
...
-- Looking for __system_property_get
-- Looking for __system_property_get - not found
-- Found Nodejs: /opt/node-19.8/include (found version "19.8.1")
-- version: 19.8.1
-- libraries: /opt/node-19.8/lib/libnode.so.111
-- uv.h: /opt/node-19.8/include/node
-- v8config.h: /opt/node-19.8/include/node
-- Building in plugin: zeekjs (/home/user/zeek/auxil/zeekjs)
...
$ make -j
...
$ sudo make install
To test if the plugin is available on a given Zeek installation, run zeek -N Zeek::JavaScript
.
The zeek
executable will also be dynamically linked against libnode.so
.
$ zeek -NN Zeek::JavaScript
Zeek::JavaScript - Experimental JavaScript support for Zeek (built-in)
Implements LoadFile (priority 0)
$ ldd $(which zeek) | grep libnode
libnode.so.111 => /opt/node-19.8/lib/libnode.so.111 (0x00007f281aa25000)
The main hooking mechanism used by the plugin is loading files with .js
and .cjs
suffixes.
If no such files are provided on the command-line or via @load
, neither
the Node.js environment nor the V8 JavaScript engine will be initialized and there
will be no runtime overhead of having the plugin available. When JavaScript
code is loaded, additional overhead may come from processing JavaScript’s IO
loop or running garbage collection.
Hello World¶
When JavaScript is executed by Zeek, a zeek
object is added to
the JavaScript’s global namespace.
This object can be used to register event or hook handlers, raise new Zeek
events, invoking Zeek side functions, etc. This is similar to the global
document
object in a browser, but for Zeek functionality.
The API documentation for the global zeek
object created is available
in the ZeekJS documentation.
The following script calls the zeek_version
built-in
function and uses JavaScript’s console.log()
for printing a Hello message
within a zeek_init
handler:
// hello.js
zeek.on('zeek_init', () => {
let version = zeek.invoke('zeek_version');
console.log(`Hello, Zeek ${version}!`);
});
$ zeek js/hello.js
Hello, Zeek 6.0.0!
Types¶
JavaScript doesn’t support types as rich as Zeek and is further dynamically
typed. As of now, most atomic types like addr
or subnet
are created as JavaScript strings or another primitive type.
For example, values of type count
become JavaScript BigInt values.
time
and interval
are converted to numbers representing
seconds with time
representing the Unix timestamp.
A list of type conversions implemented is presented in the following table.
Zeek |
JavaScript |
bool |
boolean (true, false) |
count |
|
int |
|
double |
|
interval |
Number as seconds |
time |
Number as unix timestamp in seconds |
string |
string (latin1 encoding assumed) |
enum |
string |
addr |
string |
subnet |
string |
port |
Object with |
vector |
|
set |
|
table |
Object holding a reference to a Zeek table value |
record |
Object holding a reference to a Zeek record value |
Some type conversions are not implemented, they’ll cause an error message
and have a null
value in JavaScript. pattern
values is one
such example.
Note
These type conversions may change in the future or become configurable via callbacks.
Record values¶
Record values are passed by reference from Zeek to JavaScript. That is, JavaScript objects keep a pointer to the Zeek record they represent. Holding a JavaScript object referencing a Zeek record value will keep it alive within Zeek even if Zeek itself does not reference it anymore. Updates to fields in Zeek become visible within JavaScript. Updates to properties of such objects in JavaScript become visible in Zeek.
On the other hand, normal JavaScript objects ({} or Object()) are passed
from JavaScript to Zeek are passed by copy as new Zeek record values. Changes
to the original JavaScript object will not be reflected within Zeek.
In the example below, the intel_item
JavaScript object will be converted to
a new Intel::Item
Zeek record which is then
passed to the Intel::insert
function. Modifying properties of
intel_item
after it has been inserted to the Intel data store has
no impact.
// intel-insert.js
zeek.on('zeek_init', () => {
let intel_item = {
indicator: '192.168.0.1',
indicator_type: 'Intel::ADDR',
meta: { source: 'some intel source' },
};
zeek.invoke('Intel::insert', [intel_item]);
});
Note
The background to this is that Zeek’s base has no knowledge of anything JavaScript related, while the ZeekJS plugin does have intimate knowledge about Zeek values and internals.
Table values¶
Table values are treated very similar to records. JavaScript objects representing table values keep a reference to the Zeek value. Accessing multi-index Zeek tables from JavaScript is not supported, however, as there’s no easy way to translate Zeek’s multi-value keys to properties or map keys in JavaScript.
Global tables can be modified from JavaScript directly through the zeek.global_vars
object.
The following script provides an example how to change the content
of Conn::analyzer_inactivity_timeouts
in JavaScript.
The update to the table becomes visible on the Zeek side and will be
in effect for future connections.
// global-vars.js
const timeouts = zeek.global_vars['Conn::analyzer_inactivity_timeouts'];
// Similar to redef.
timeouts['AllAnalyzers::ANALYZER_ANALYZER_SSH'] = 42.0;
zeek.on('zeek_init', () => {
console.log('js', timeouts);
});
$ zeek global-vars.js -e 'event zeek_init() &priority=-5 { print "zeek", Conn::analyzer_inactivity_timeouts; }'
js {
[AllAnalyzers::ANALYZER_ANALYZER_SSH]: 42,
[AllAnalyzers::ANALYZER_ANALYZER_FTP]: 3600
}
zeek, {
[AllAnalyzers::ANALYZER_ANALYZER_SSH] = 42.0 secs,
[AllAnalyzers::ANALYZER_ANALYZER_FTP] = 1.0 hr
}
Set and vector values¶
The set
and vector
types are currently copied from
Zeek to JavaScript as Array objects. These objects don’t reference the
original set or vector on the Zeek side. This means that mutation of the
JavaScript side objects via accessors on the Array do not modify the
Zeek side value. However, objects referencing the Zeek record values within
these arrays are mutable.
This mainly becomes relevant if you wanted to modify state attached to
a connection within JavaScript. Re-assigning c.service
below works
as expected, the c.service.push()
approach on the other had would
not change the set on the Zeek-side.
// connection-service.js
zeek.on('connection_state_remove', { priority: 10 }, (c) => {
// c.service.push('service-from-js'); only modifies JavaScript array
c.service = c.service.concat('service-from-js');
});
zeek.hook('Conn::log_policy', (rec, id, filter) => {
console.log(rec.service);
});
$ zeek -r ../../traces/get.trace ./connection-service.js
service-from-js,http
Note
The current approach was mostly chosen for implementation simplicity and the assumption that modifying Zeek side vectors or sets from JavaScript is an edge case. This may change in the future.
Any and zeek.as()¶
Some of Zeek’s function take a value of type any
. This makes it
impossible to implicitly convert from a JavaScript type to the appropriate
Zeek type.
The function zeek.as()
can be leveraged within JavaScript to create an
object given a JavaScript value and a Zeek type name. That object is then
referencing a Zeek value and when used to call a function taking an any
parameter, the plugin directly threads through the referenced Zeek value
and the call succeeds.
// zeek-as.js
zeek.on('zeek_init', () => {
try {
// This throws because type_name takes an any parameter
zeek.invoke('type_name', ['192.168.0.0/16']);
} catch (e) {
console.error(`error: ${e}`);
}
// Explicit conversion of string to addr type.
let type_string = zeek.invoke('type_name', [zeek.as('subnet', '192.168.0.0/16')]);
console.log(`good: type_name is ${type_string}`);
});
The first call to zeek.invoke()
throws an exception due to the failing
type conversion, the second one succeeds.
$ zeek -B plugin-Zeek-JavaScript zeek-as.js
error: Unable to convert JS value '192.168.0.0/16' of type string to Zeek type any
good: type_name is subnet
Debugging¶
There might be limitations, surprises and bugs with the type conversions.
If Zeek was built with debugging enabled, the plugin-Zeek-JavaScript
debug stream may provide some helpful clues.
$ ZEEK_DEBUG_LOG_STDERR=1 zeek -B plugin-Zeek-JavaScript hello.js
0.000000/1685018723.447965 [plugin Zeek::JavaScript] Hooked .js file=hello.js (./hello.js)
0.000000/1685018723.457376 [plugin Zeek::JavaScript] Hooked 1 .js files: Initializing!
0.000000/1685018723.457639 [plugin Zeek::JavaScript] Init: Node initialized. Compiled with v19.8.1
0.000000/1685018723.458774 [plugin Zeek::JavaScript] Init: V8 initialized. Version 10.8.168.25-node.12
0.000000/1685018723.539618 [plugin Zeek::JavaScript] ExecuteAndWaitForInit: init() result=object 1
0.000000/1685018723.539644 [plugin Zeek::JavaScript] ExecuteAndWaitForInit: zeek_javascript_init returned promise, state=0 - running JS loop
0.000000/1685018723.551058 [plugin Zeek::JavaScript] Registering zeek_init priority=0, js_eh=0x603001cac710
0.000000/1685018723.551120 [plugin Zeek::JavaScript] Registered zeek_init
1685018723.601898/1685018723.621106 [plugin Zeek::JavaScript] ZeekInvoke: invoke for zeek_version
1685018723.601898/1685018723.621177 [plugin Zeek::JavaScript] Invoke zeek_version with 0 args
1685018723.601898/1685018723.621212 [plugin Zeek::JavaScript] ZeekInvoke: invoke for zeek_version returned:
Hello, Zeek 6.0.0-dev.636-debug!
1685018723.644485/1685018723.644726 [plugin Zeek::JavaScript] Done...
1685018723.644485/1685018723.644754 [plugin Zeek::JavaScript] Done: uv_loop not alive anymore on iteration 0
Examples¶
HTTP API¶
The following JavaScript file provides an HTTP API for generically invoking
Zeek functions and Zeek events using curl
. It’s 60 lines of vanilla
Node.js JavaScript (with limited error handling), but allows for experiments
and runtime reconfiguration of a Zeek process that’s hard to achieve with
Zeek provided functionality. Essentially, all that is used is zeek.event
and zeek.invoke
and relying on implicit type conversion to mostly do
the right thing.
The two supported endpoints are /events/<event_name>
and /functions/<function_name>
. Arguments are passed in an args
array
as JSON in the POST request’s body.
## api.zeek
##
## Sample events to be invoked by api.js
module MyAPI;
export {
global print_msg: event(msg: string, ts: time &default=network_time());
}
event MyAPI::print_msg(msg: string, ts: time) {
print "ZEEK", "print_msg", ts, msg;
}
@load ./api.js
// api.js
//
// HTTP API allowing to invoke any Zeek events and functions using a simple JSON payload.
//
// Triggering and intel match (this will log to intel.log)
//
// $ curl --data-raw '{"args": [{"indicator": "50.3.2.1", "indicator_type": "Intel::ADDR", "where":"Intel::IN_ANYWHERE"}, []]}' \
// http://localhost:8080/events/Intel::match
//
// Calling a Zeek function:
//
// $ curl -XPOST --data '{"args": [1000]}' localhost:8080/functions/rand
// {
// "result": 730
// }
//
const http = require('node:http');
// Light-weight safe-json-stringify replacement.
BigInt.prototype.toJSON = function () { return parseInt(this.toString()); };
const handleCall = (cb, req, res) => {
const name = req.url.split('/').at(-1);
const body = [];
req.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
try {
const parsed = JSON.parse(Buffer.concat(body).toString() || '{}');
const args = parsed.args || [];
const result = cb(name, args);
res.writeHead(202);
return res.end(`${JSON.stringify({ result: result }, null, 2)}\n`);
} catch (err) {
console.error(`error: ${err}`);
res.writeHead(400);
return res.end(`${JSON.stringify({ error: err.toString() })}\n`);
}
});
};
const server = http.createServer((req, res) => {
if (req.method === 'POST') {
if (req.url.startsWith('/events/')) {
return handleCall(zeek.event, req, res);
} else if (req.url.startsWith('/functions/')) {
return handleCall(zeek.invoke, req, res);
}
}
res.writeHead(404);
return res.end();
});
const host = process.env.API_HOST || '127.0.0.1';
const port = parseInt(process.env.API_PORT || 8080, 10);
server.listen(port, host, () => {
console.log(`Listening on ${host}:${port}...`);
});
$ zeek -C -i lo ./api.zeek
Listening on 127.0.0.1:8080...
listening on lo
As a first example, the get_net_stats
built-in function is
invoked and returns the current monitoring statistics in response.
$ curl -XPOST http://localhost:8080/functions/get_net_stats
{
"result": {
"pkts_recvd": 3558,
"pkts_dropped": 0,
"pkts_link": 7126,
"bytes_recvd": 27982155
}
}
Posting to /events/MyAPI::print_msg
raises the MyAPI::print_msg
event
implemented in the api.zeek
file.
$ curl -4 --data-raw '{"args": ["Hello Zeek!"]}' http://localhost:8080/events/MyAPI::print_msg
{}
# The Zeek process will output:
ZEEK, print_msg, 1685121096.892404, Hello Zeek!
It is possible to runtime disable (and enable) analyzers as well by
leveraging Analyzer::disable_analyzer
. Here shown for the SSL analyzer.
$ curl -XPOST --data '{"args": ["AllAnalyzers::ANALYZER_ANALYZER_SSL"]}' localhost:8080/functions/Analyzer::disable_analyzer
{
"result": true
}
Todo
Using Analyzer::ANALYZER_SSL
is currently not possible due to
Analyzer::disable_analyzer
taking an AllAnalyzers::Tag
and the enum names are different.
As a fairly advanced example, creating a new Log::Filter
instance
for the Conn::LOG
stream at runtime using Log::add_filter
is possible. Removal works, too.
$ curl -XPOST --data '{"args": ["Conn::LOG", {"name": "my-conn-rotate", "path": "my-conn-rotate", "include": ["ts", "id.orig_h", "id.res_h", "history"], "interv": 10}]}' \
localhost:8080/functions/Log::add_filter
{
"result": true
}
$ curl -XPOST --data '{"args": ["Conn::LOG", "my-conn-rotate"]}' localhost:8080/functions/Log::remove_filter
{
"result": true
}
This API can also be used to invoke terminate
, so you want to be
careful deploying this in an actual production environment:
$ curl -XPOST --data '{"args": []}' localhost:8080/functions/terminate
{
"result": true
}
# Zeek is now stopping with:
1685121663.854714 <params>, line 1: received termination signal
1685121663.854714 <params>, line 1: 53 packets received on interface lo, 0 (0.00%) dropped, 0 (0.00%) not processed
More¶
More examples can be found in the ZeekJS documentation and repository.
TypeScript¶
TypeScript adds typing to JavaScript. While ZeekJS has no TypeScript awareness,
there’s nothing preventing you from using it. Use tsc
for type checking and
provide the produced .js
files to Zeek.
You may need a zeek.d.ts
file for the zeek
object. A bare
zeek.d.ts file has been
tested, but not integrated with ZeekJS at this point.