Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Detection-as-code with babysoarus-ci

Testing a detection normally needs nothing running: no Splunk, no daemon, no network. babysoarus-ci loads the same runtime the editor's Tests panel and the built-in library both use, directly, as a single binary -- which is what makes running your detections in a pull request practical instead of a project of its own.

Getting the Pieces

Two things, and they come from different places.

An installed app directory, for the Python executor and your own saved functions. Any real install already has one -- point --app (or BABYSOARUS_APP_DIR) at $SPLUNK_HOME/etc/apps/babysoarus, or at wherever babysoarus.spl was extracted for a CI job that has no Splunk to install into at all.

The babysoarus-ci binary itself, which ships in the same package, at bin/babysoarus-ci. So both pieces come from the one babysoarus.spl you already have: extract it, and the app directory and the binary are both inside. A CI job needs no toolchain, no Splunk and no network -- one archive, one extraction, done.

export BABYSOARUS_APP_DIR=/path/to/installed/BabySOARus
babysoarus-ci --version

Running One Thing

echo '[{"n": 5}]' > events.json
babysoarus-ci run --inline "for e in events:
    e['doubled'] = int(e['n']) * 2" --events events.json

--function NAME works the same way in place of --inline CODE, against a saved function instead of a snippet typed on the command line. This is the primitive everything else is built from, and on its own it is enough to git bisect a detection that used to pass and does not any more.

Running a Test Suite

babysoarus-ci test tests/

Test definitions are exactly the test.json shape the editor uses -- the same file that runs in the browser runs here unchanged, because a test that only exists in a KV store cannot be reviewed in a pull request:

{
  "name": "doubles the value",
  "inline": "for e in events:\n    e['doubled'] = int(e['n']) * 2",
  "events": [{ "n": 5 }],
  "expect": { "count": 1, "contains": [{ "n": 5, "doubled": 10 }] }
}

A path may be a single definition, an array of them in one file, or a directory -- walked recursively, entries sorted, so two identical runs report in the same order.

Exit codes are the whole API, and 1 and 2 mean different things on purpose: 0 every test passed, 1 a test failed, 2 the tool could not even run (a missing app directory, an unreadable file). A CI system that cannot tell "your detection is broken" from "the runner is misconfigured" shows both as the same red cross. --json prints machine-readable output instead of the human-readable pass/fail list, for a job that wants to parse the result rather than grep it.

A definition that cannot even start (a typo in function, malformed JSON) is reported as that one test failing, not as the whole run aborting -- one bad file never hides the results of every good one.

Benchmarking, Honestly

# Once, to record a baseline:
babysoarus-ci bench --function hunting/beacon_jitter --events events.json \
  --samples 30 --save baseline.json

# On every later run, to gate against it:
babysoarus-ci bench --function hunting/beacon_jitter --events events.json \
  --samples 30 --max-noise 10 --baseline baseline.json

Every figure is a median with its 10th and 90th percentiles, never a bare number, and a null control -- the same function measured as both sides of a comparison -- runs first, so the noise floor is on screen before any figure that might be judged against it. --max-noise refuses to report at all once that floor is too high to trust; --baseline then fails only once a difference is larger than both runs' noise floors combined, not on any slowdown at all, which is what keeps a regression gate usable on shared CI hardware rather than disabled within a week for crying wolf.

In a Pipeline

- name: babysoarus-ci
  run: |
    babysoarus-ci test tests/

The whole point is that this step needs nothing else in the job: no Splunk service container, no network egress, no Python interpreter on the runner -- the one inside the WebAssembly component is the only one that ever runs your code, sandboxed exactly as it is in production.

Not Built Yet

babysoarus-ci fuzz and babysoarus-ci lint are named in the usage text and refused outright rather than treated as typos -- the message says planned, not built yet rather than unknown command.