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

Dependencies

Inline snippets can use third-party Python packages. There are two ways to get one, and they differ in when the work happens rather than in what you end up with:

QuestionInstalled at run timeVendored at build time
You editconfig/packages.json on the search headrequirements.txt in the source tree
Takes effectnext daemon start, no rebuildafter make python precompile and a reinstall
Needsnetwork access from the search head to an indexa build machine
Best foradding a package to a deployment you have already shippedpackages every deployment should have

Pure Python only, either way. A package that ships a compiled extension module is built for a physical CPU, and the sandbox is WebAssembly. Both paths detect this and refuse, rather than producing something that fails at search time. See What will not work.

Installing a Package on a Running Deployment

Write config/packages.json inside the app directory:

{
  "packages": [
    { "name": "jmespath", "version": "1.0.1" },
    { "name": "python-dateutil", "version": "2.9.0.post0" }
  ]
}

The daemon reads it at startup, downloads each wheel, checks its SHA-256 against what the index published, unpacks it into packages/, and records what it did in packages/installed.json. Restart the daemon and the package is importable:

pkill -f etc/apps/babysoarus/bin/babysoarus-daemon

There is no need to restart Splunk: the next search starts the daemon again, and it finishes reconciling before it accepts a connection, so the first search after the restart already sees the package. Watch it happen in $SPLUNK_HOME/etc/apps/babysoarus/var/daemon.log:

INFO installed Python package package=jmespath version=1.0.1 files=8
INFO reconciled Python packages installed=1 removed=0 unchanged=0 failed=0
INFO precompiled Python bytecode compiled=8 failed=0 elapsed_ms=41

That last line is the daemon compiling the package's bytecode straight away, so the first search does not have to. The result is cached in var/pycache/, is regenerable, and is removed along with the package if you withdraw it.

A package that cannot be installed is logged as an error and the daemon still starts. That is deliberate: one bad entry should cost you that package, not every search on the search head.

The file is declarative: the daemon makes packages/ match it. Removing an entry uninstalls it, changing a version replaces it, and running with the same file twice does nothing the second time. Only files the manifest records are ever deleted, so anything you place in packages/ by hand survives.

No file and an empty file mean different things. {"packages": []} means "manage this, and it should be empty", so it uninstalls everything. Deleting config/packages.json altogether means "do not manage this", and leaves what is already installed alone. If you want the packages gone, empty the list rather than deleting the file.

Versions Are Exact, on Purpose

"version": "1.0.1" is required. Ranges such as >=1.0 are refused, because two search heads reconciling a week apart would then install different code from the same configuration. If you want to be stricter still, pin the wheel itself:

{ "name": "jmespath", "version": "1.0.1", "sha256": "02e2e4cc71b5bcd8..." }

Then the index must offer that exact wheel, or nothing is installed.

Using an Internal Index

Most enterprise search heads cannot reach pypi.org. Point at a mirror:

{
  "index_url": "https://pypi.internal.example.com",
  "packages": [{ "name": "jmespath", "version": "1.0.1" }]
}

The URL must speak the PyPI JSON API, which is what Artifactory, Nexus and devpi all provide: the daemon reads <index_url>/pypi/<name>/<version>/json.

If the mirror presents a certificate signed by your own CA, point the daemon at the bundle:

export BABYSOARUS_CA_BUNDLE=/etc/pki/tls/certs/corporate-ca.pem

The daemon trusts the public roots and the system trust store already; this adds to them. It is validated when read, so a path that does not exist, or a file with no certificates in it, is an error at startup rather than a confusing TLS failure later.

Why not SSL_CERT_FILE? Splunk sets that variable in every process it starts, and points it at its own trust store. On some versions it points at a file that is not shipped at all. BabySOARus therefore ignores it and uses BABYSOARUS_CA_BUNDLE instead, so that what the daemon trusts is something you chose rather than something you inherited.

The Network Policy Applies to Installs Too

If config/acl.json is present and enforcing, the index host must be permitted by it. The daemon will not fetch from a host you have denied to guest code, and says so rather than failing quietly:

the network policy does not permit pypi.org, so packages cannot be installed
from https://pypi.org. Add it to allow_hosts in config/acl.json, or point
index_url at a mirror that is permitted

With no policy file at all, egress is unrestricted and so is the installer.

Turning Installation Off without Deleting the File

{
  "enabled": false,
  "packages": [{ "name": "jmespath", "version": "1.0.1" }]
}

enabled defaults to true, so omitting it installs. Setting it to false stops the daemon reconciling from the file while leaving the list intact, which is what you want when isolating whether a package is behind a problem. It does not uninstall anything already in packages/; removing an entry does that.

What Guest Code Sees

packages/ is preopened read-only at /packages and appended to sys.path. Read-only is deliberate: code that could write there could replace the module the next search imports. Bytecode goes to a separate writable directory instead, so importing a package does not recompile it every run.

Because /packages is appended rather than prepended, an installed package can never shadow the standard library or a build-time vendored package. A package named json will not change what json means in an existing snippet.

Adding a Package at Build Time

Add it to python/exec_component/requirements.txt:

python-dateutil==2.9.0.post0
jmespath==1.0.1

Then rebuild:

make python precompile
SPLUNK_HOME=/opt/splunk make install

That is it. The package is now importable:

| makeresults
| eval doc="{\"a\": {\"b\": [10, 20, 30]}}"
| exec inline="import jmespath
for e in events:
    e['picked'] = jmespath.search('a.b[1]', json.loads(e['doc']))"
           _time                       doc             picked
--------------------------- -------------------------- ------
2026-07-31 21:34:51.000 BST {"a": {"b": [10, 20, 30]}}     20

Vendored packages are also in scope without importing them, so jmespath.search(...) works directly.

What the Build Actually Does

make python runs python/vendor-deps.py, which:

  1. installs your requirements with pip install --target python/exec_component/vendor, falling back to uv pip install --target if pip is not on the path;
  2. scans the result for compiled extension modules and stops if it finds any;
  3. writes python/exec_component/_vendored.py, a generated module containing one import line per vendored package;
  4. passes -p . -p vendor to componentize-py so both directories are on the interpreter's path.

Step 3 is the one that matters. componentize-py resolves imports when the component is built and snapshots the interpreter's memory, so a package that merely exists on disk is not importable at search time. The generated module forces every vendored package to be imported during the build, which puts it in the snapshot.

# python/exec_component/_vendored.py, generated
import jmespath

VENDORED = {
    "jmespath": jmespath,
}

You never edit that file. It is regenerated from requirements.txt.

Doing It by Hand

If you would rather drive the install yourself:

pip install --target python/exec_component/vendor jmespath==1.0.1
python3 python/vendor-deps.py     # regenerates _vendored.py from what is there
make python precompile

vendor-deps.py reinstalls from requirements.txt when run on its own, so keep the file in step with what you want vendored.

What Will Not Work

Anything with a .so, .pyd or .dylib in it. The build says so plainly:

these vendored files are compiled for a native CPU and cannot run inside
WebAssembly:
  orjson/orjson.cpython-312-x86_64-linux-gnu.so

Only pure-Python packages can be vendored. Look for a pure-Python alternative,
or move the work to the host side of a custom .wasm function. Pass
--allow-native to install anyway.

The usual suspects, and what to do instead:

PackageWhy notInstead
numpy, pandas, scipyCompiledstatistics, or a Rust custom function
requests, httpxNeed real socketshttp_request, which the host performs
cryptography, pyOpenSSLCompiledhashlib, hmac, secrets
orjson, ujsonCompiledjson, already present
lxmlCompiledxml.etree.ElementTree, already present
psycopg, pymysqlNeed socketsQuery via an HTTP API, or a custom function
regexCompiledre, already present

A useful test before you try: if the wheel on PyPI is named something-1.2.3-py3-none-any.whl, it is pure Python and will work. If it is named something-1.2.3-cp314-cp314-manylinux_x86_64.whl, it will not: a cp tag means compiled code, whichever version follows it.

Packages Worth Vendoring

These are pure Python, small, and genuinely useful in a SOC context:

PackageFor
python-dateutilParsing the timestamp formats datetime refuses
jmespathQuerying deeply nested JSON without ten .get() calls
tldextractCorrect public-suffix handling for domains
pyparsingGrammar-based parsing of odd log formats
cbor2CBOR payloads
chardetCharacter-set detection on messy fields
stix2-patternsValidating STIX patterns from a feed

Costs

Every vendored package increases the component image, its build time, and the memory each pooled instance maps. The interpreter image is about 33 MB before any dependencies. A handful of small pure-Python packages adds a megabyte or two and is not worth worrying about; vendoring something enormous is.

Because the image is mapped copy-on-write into each pooled instance, size costs address space rather than per-search CPU. It does not slow searches down. It does make the build take longer.

Keeping It Reproducible

Pin exact versions in requirements.txt. The vendor directory is generated and should not be committed, but requirements.txt and the generated _vendored.py describe exactly what a build will contain, so both belong in version control if you want reproducible interpreters across a fleet.