flabel's tests invoke Zeek, Suricata and editcap for real — there are no mocks and no
golden-file substitutes, because a mock would encode our assumptions about tool behaviour,
which is exactly what needs verifying (docs/spec.md §2). So the toolchain is a hard test
dependency, not an optional extra.
uv sync --locked # dev deps only — flabel itself has zero runtime dependencies
uv run pytest -q
uv run ruff check . && uv run ruff format .brew install zeek suricata wiresharkwireshark supplies editcap and capinfos. Verify:
zeek --version # zeek version 8.0.x
suricata -V # This is Suricata version 8.0.x RELEASE
editcap --versionsuricata --version does not exist — it exits 1 with "unrecognized option". -V is the
flag. Worth knowing before you conclude the install is broken.
JA4 is computed by Zeek, and the JA4 value carried on a label is always the Zeek-computed
one, so Zeek is the single authority (docs/prd.md §9). That needs the zeek/foxio/ja4
package, installed with zkg.
Homebrew's zkg ships without its Python dependencies, and fails like this:
error: zkg failed to import one or more dependencies:
* GitPython * semantic-version
Fix it, then install the package:
pipx install zkg # see the note below on why not pip --user
zkg autoconfig
zkg install --version v0.18.8 zeek/foxio/ja4
zeek --parse-only -e '@load ja4' # should exit 0
zkg list # should show "installed: v0.18.8"@load ja4 is the same check the tests use, deliberately: if it fails while the package
directory exists, the package is installed somewhere Zeek won't load it, which is the
problem worth knowing about.
Why pipx and not pip3 install --user. zkg's shebang is #!/usr/bin/env python3,
which resolves through PATH. Under uv run the project virtualenv comes first, and a
virtualenv ignores the user site directory — so pip3 install --user GitPython fixes zkg
in your shell and leaves it broken when a test calls it. pipx gives zkg its own
interpreter with its own dependencies, immune to whatever PATH it is called through. The
CI image solves the same problem differently, by binding zkg's shebang to /usr/bin/python3.
If zkg still isn't usable, test_installed_ja4_version_matches_the_pin skips locally and
says so. That is expected on a laptop; in CI the same condition fails.
Licence note.
zeek/foxio/ja4is JA4+ under the FoxIO License 1.1 (non-commercial). It is the approved default while Legal's review proceeds; restricting to plain JA4 (BSD 3-Clause) is the documented contingency if Legal declines (docs/prd.md§13 Q3). Do not treat it as an ordinary open-source dependency.
Until this is installed, test_zeek_loads_ja4_package skips locally. CI runs with
--strict-toolchain, where the same condition is a hard failure — so the container can
never quietly ship without it.
CI runs inside the digest-pinned image built by Dockerfile.toolchain, and adds two flags:
| Flag | Effect |
|---|---|
--require-tool-tests |
The run fails unless a requires_tools test actually ran and passed, and fails if any was deselected. An xfailed or mid-body-skipped tool test does not count — a suite that skipped the integration layer must never look like a passing one. |
--strict-toolchain |
Tool versions must match [tool.flabel.toolchain] in pyproject.toml exactly, and the JA4 package must be present. |
Locally, neither flag is passed: versions are checked at major.minor only, so a brew upgrade bumping a patch version doesn't turn your suite red.
A legitimate local skip names the missing tool:
SKIPPED [1] tests/test_toolchain.py: toolchain not installed: suricata — see docs/dev-setup.md
If you see that in CI, something is wrong with the image, not with the test — CI's
--require-tool-tests should have turned it into a failure before it could be mistaken for
success.
Take the digest from the container: line in .github/workflows/ci.yml:
docker run --rm -v "$PWD":/work -w /work \
-e UV_PROJECT_ENVIRONMENT=/tmp/venv \
ghcr.io/deeptempo/flabel-toolchain@sha256:<digest> \
sh -c 'uv sync --locked --dev && uv run pytest -q --require-tool-tests --strict-toolchain'Don't add --lf, --ff, or -k to that command: with --require-tool-tests they deselect
tool tests, and deselection fails the gate on purpose. Drop --require-tool-tests when you
want to narrow a run while debugging.
UV_PROJECT_ENVIRONMENT matters: without it, uv sync replaces your macOS/arm64 .venv
with a root-owned linux/amd64 one inside the bind mount, and your next host-side
uv run pytest fails confusingly. For the same reason, expect root-owned __pycache__
directories if you run this often — add --user "$(id -u):$(id -g)" to avoid them.
The image is amd64-only, because the openSUSE Build Service repo that packages Zeek publishes no arm64 build. On Apple silicon it runs under emulation — correct but slow, so it's for reproducing a CI failure, not for everyday work. Use the brew toolchain for that.
docker run --rm <image> cat /etc/flabel-toolchain.json prints the exact versions the image
was built with.
[tool.flabel.toolchain] in pyproject.toml is the single source of truth. It records what
Dockerfile.toolchain installs, which is the reproducibility contract for Goal 2 — identity
across unpinned tool versions means nothing.
To bump the toolchain:
- Edit the
ARGvalues inDockerfile.toolchain— the exact apt versions (SURICATA_PACKAGE_VERSION,WIRESHARK_PACKAGE_VERSION,ZEEK_PACKAGE_VERSION) and, for JA4, both the tag and the commit it must resolve to. - Edit the matching variables in
docs/phase-2-replay-box-provision.sh. There is a third copy of these pins, becausefl-replayinstalls from apt rather than from the image — andfl-replayis the only machine that produces ground truth. This step was missing from this list until 2026-08-27, which is how the box came to run Suricata 7.0.3 against an 8.0 ruleset for weeks with CI green (#142).test_the_provisioning_script_pins_match_toonow fails if you skip it, so this is a reminder rather than the guard. - Update
[tool.flabel.toolchain]in the same commit. Thetoolchainworkflow runs the suite inside the new image with--strict-toolchain, so a Dockerfile bump without a matching pin bump fails there rather than silently publishing. - Push; the workflow prints the new digest. Put it in
ci.yml'scontainer:line.
The apt repositories serve only the newest patch of each release line, so an upstream patch release eventually makes a pinned version unavailable and the image build fails. That is intended: a toolchain change should be a visible, deliberate commit, never a silent drift.
The pins now install exact versions rather than describing a past build, and the base image,
uv, and the JA4 commit are all pinned. But the apt repositories do not keep old patches, so
Dockerfile.toolchain will eventually stop being rebuildable, and the pinned environment
survives only as the GHCR image referenced by digest from ci.yml. Reproducing a labelling
run older than that image's retention means keeping the image, not rebuilding it. See
docs/status.yaml known_gaps.
ci.yml pulls the image with credentials:, because the package is private. It contains
the JA4+ package, which is FoxIO License 1.1 (non-commercial), and republishing it in a
public image is not a call to make while Legal's review is open. Consequence: a pull request
from a fork cannot pull the image and its test job will fail. There are no external
contributors today; revisit if that changes.