JavaScript

Added in version 6.0.

Note

Zeek’s JavaScript support started out as an independent Zeek package called ZeekJS, which features additional documentation.

Note

The JavaScript integration does not provide Zeek’s typical backwards compatibility guarantees at this point. That said, we’ll avoid unnecessary breakage.

Preamble

If you’d like to integrate Zeek with external systems, Zeek offers several options. Developers can extend Zeek by implementing C++ plugins. The system function lets you invoke external programs from Zeek scripts. The Input Framework supports flexible data ingestion via its raw reader, and Zeek’s WebSocket support allows exchanging events between Zeek and external programs.

Zeek’s JavaScript support 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’d like to start a HTTP server within Zeek, record Zeek event data on-the-fly to a Redis database, speak to a REST API in your infrastructure, or simply are quite familiar with JavaScript, then keep reading!

Enabling JavaScript

JavaScript support is an optional Zeek feature. When Node.js development headers and libraries are found when building Zeek from source, the plugin is automatically included. Our build instructions cover the required dependencies for a range of distributions and operating systems. Our Docker images support it out-of-the-box. 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 I/O loop or running garbage collection.

Nonstandard Builds

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.

$ ./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

Hello World

When Zeek executes JavaScript, it adds a zeek object to the JavaScript runtime’s global namespace. You can use this object to register event or hook handlers, raise new Zeek events, invoke 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
// hello.js
zeek.on('zeek_init', () => {
  let version = zeek.invoke('zeek_version');
  console.log(`Hello, Zeek ${version}!`);
});
$ zeek js/hello.js
Hello, Zeek 8.0.0!

Execution Model

Zeek executes JavaScript code in two ways. We’ll have to get a bit technical here for a moment to explain them, but don’t worry if the details don’t make sense right now!

First, JavaScript event or hook handlers are added as additional Func::Body instances to the respective Func objects. These extra bodies point to instances of a custom Stmt subclass with tag STMT_EXTERN. The Stmt::Exec() implementation of this class calls the listener function, a v8::Function, registered through zeek.on(). When Zeek executes all bodies of an event or hook handler during Func::Invoke(), some bodies execute JavaScript functions instead of Zeek script statements. This approach allows to register JavaScript listener functions using Zeek’s priority mechanism. Further, changes done by JavaScript code to global Zeek variables or record fields are visible to Zeek script and vice versa. In summary, execution of Zeek and JavaScript code is interleaved when executing event or hook handlers.

Second, the Node.js IO loop (libuv) is registered as an IOSource with Zeek’s main loop. When there’s any IO activity in Node.js, libuv’s backend file descriptor becomes ready, waking up the Zeek main loop. Zeek then transfers control through the registered IOsource to the JavaScript plugin which runs the libuv IO loop until there’s no more work to be done. At this point, the plugin yields control back to Zeek’s main loop, draining any queued events, processing timers, or simply waiting for the next network packet to arrive.

From the above it follows that there is no parallel JavaScript code execution happening in a separate thread. Zeek script and JavaScript execute interleaved on Zeek’s main thread, driven by the main loop’s logic. This also implies that long running JavaScript code will block Zeek’s main loop and Zeek script execution. This is no different than what would happen in a web browser or an asynchronous Node.js network server, however, and the same applies to a long running Zeek script event handler.

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.

Type Conversions

Zeek

JavaScript

bool

boolean (true, false)

count

BigInt

int

Number

double

Number

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 port an proto properties and a custom toJSON() method only returning the port

vector

Copied as Array, see below

set

Copied as Array, see below

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.

Number values

The default JavaScript number type is implemented as a double precision floating point values. This means that some numbers within Zeek, specifically int values greater than 2**53 -1 or less than -(2**53-1) loose precision when converted to JavaScript. Additionally, converting from a JavaScript number to a Zeek native type may throw TypeError exceptions at runtime when the JavaScript number is not representable as a zeek_int_t or zeek_uint_t. Concretely, this happens when converting numeric values greater than 2**64-1 or less than 0 to count, or for int conversions involving numbers greater than 2**63-1 and less than -(2**63-1). Attempting to convert JavaScript numbers with fractional parts to int or count also results in TypeError exceptions.

Zeek count values are always converted to BigInt in JavaScript. For interacting with Zeek, the recommendation is to use BigInt on the JavaScript side if the expected numeric range exceeds JavaScript’s Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER constants.

If you’re using JavaScript to implement a basic configuration system for Zeek, these restrictions may not be problematic. However, packet counters or number of bytes transferred can exceed 2**53-1 in fairly short amounts of time.

Finally, Zeek script cannot represent numeric values larger than 2**64-1 other than converting them to string or double values, which is either cumbersome or involves precision loss.

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 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
// 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
// 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 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
// 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.pcap  ./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-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
## 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
// 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.