ParseZeekLogs
Read Zeek network security monitor logs into typed Python records, convert them to JSON or CSV, map them to Elastic Common Schema, and load them into Elasticsearch.
Both Zeek output formats are handled by the same reader: the default
tab-separated logs, whose #fields/#types header drives full type
conversion, and JSON logs (LogAscii::use_json=T). Gzip-compressed files and
concatenated rotated logs work too.
Install
Requires Python 3.10 or newer. The core package has no dependencies.
pip install parsezeeklogs
Add the Elasticsearch loader (client 9.x, for Elasticsearch 8 and 9):
pip install 'parsezeeklogs[elasticsearch]'
Command line
parsezeeklogs json FILE... [-o OUT] [--ecs] [-f FIELDS] [--safe-headers] [-m JSON] [--time-format iso]
parsezeeklogs csv FILE... [-o OUT] [-f FIELDS] [--no-header]
parsezeeklogs fields FILE
parsezeeklogs elk FILE... URL [-i INDEX] [--ecs] [--create-index] [auth/TLS options]
Convert a log to JSON lines, or to CSV with chosen columns:
parsezeeklogs json conn.log -o conn.jsonl
parsezeeklogs csv conn.log -f ts,id.orig_h,id.orig_p,id.resp_h,id.resp_p,duration -o conn.csv
List a log's fields with their Zeek types:
parsezeeklogs fields dns.log
Load logs into Elasticsearch, creating the index with a suitable mapping:
parsezeeklogs elk conn.log dns.log http.log http://localhost:9200 -i zeek --create-index
Same, but as Elastic Common Schema documents against a secured cluster:
parsezeeklogs elk *.log https://es.example.com:9200 -i zeek-ecs --ecs --create-index --api-key "$ES_API_KEY" --ca-certs ca.pem
Options shared by json, csv and elk:
| Option | Purpose |
|---|---|
-f, --fields A,B,... |
Keep only these fields |
--safe-headers |
Rewrite dots in field names to underscores (id.orig_h becomes id_orig_h) |
-m, --meta JSON |
Merge this object into every record |
--time-format iso |
Emit time values as ISO-8601 instead of epoch seconds |
--ecs (json, elk) |
Produce Elastic Common Schema documents |
Elasticsearch options for elk: -i/--index, -s/--bulk-size,
--create-index, -u/--user, -p/--password (prompted when omitted),
--api-key, --ca-certs, -k/--insecure, --timeout. Exit status is 1 when
any bulk item failed or the cluster was unreachable. Malformed lines are
skipped and counted, not fatal.
Python API
from parsezeeklogs import ZeekLog, read_zeek
for rec in read_zeek("conn.log"):
print(rec["ts"], rec["id.orig_h"], rec["id.resp_p"], rec["duration"])
with ZeekLog("dns.log", fields=["ts", "query", "answers"], time_format="iso") as log:
print(log.path, log.types) # "dns", {"ts": "time", "query": "string", ...}
records = list(log)
print(log.skipped) # malformed lines that were reported and skipped
Values arrive typed. Zeek count, int and port become int; double,
interval and time become float; bool becomes bool; set[...] and
vector[...] become lists; unset (-) is None; empty is "" or [];
Zeek's \xHH escapes are decoded. Helpers to_json, write_json_lines and
write_csv serialise records.
Elastic Common Schema
to_ecs(record, path) returns an ECS document laid out like the Filebeat Zeek
module: source.*, destination.*, network.*, event.*, dns.*, tls.*,
file.*, user.* and so on, with everything else under zeek.<log>.* and the
Zeek uid in zeek.session_id and event.id. Community ID flow hashes,
connection state messages, network.direction, DNS answers, TLS versions and
certificate subjects are derived the same way the module's ingest pipelines do.
from parsezeeklogs import read_zeek, to_ecs
for rec in read_zeek("conn.log"):
doc = to_ecs(rec, "conn", {"observer": {"name": "sensor-1"}})
# doc["source"]["ip"], doc["event"]["duration"] (nanoseconds), doc["network"]["community_id"], ...
Every emitted field is coerced to its ECS or Beats type (long, boolean,
ip, date, array fields as lists), and --create-index --ecs builds the
index mapping from the same table, so documents and mapping always agree. The
tables come from ECS 9.5.0 and Filebeat 9.5.3; scripts/build_ecs_tables.py
regenerates them for newer releases. GeoIP, ASN and user-agent enrichment
and public-suffix based dns.question.registered_domain are not reproduced,
as they need external databases.
Elasticsearch loader
from parsezeeklogs.elastic import ZeekToElk, ensure_index, make_client
es = make_client("https://es.example.com:9200", api_key="...", ca_certs="ca.pem")
ensure_index(es, "zeek-ecs", ecs=True)
result = ZeekToElk(es, index="zeek-ecs", ecs=True, metadata={"observer.name": "sensor-1"}).load(
"conn.log"
)
print(result.indexed, result.failed, result.skipped)
Without ecs=True documents keep Zeek's field names and gain @timestamp
(from ts) and @path (the log type); the default mapping types @timestamp
as a date and id.orig_h/id.resp_h as ip.
2.x compatibility
The old iterator still works and returns JSON strings, CSV rows, or dicts:
from parsezeeklogs import ParseZeekLogs
for line in ParseZeekLogs("conn.log", output_format="json", safe_headers=True):
print(line)
ParseZeekLogs.batch_to_elk(...) is kept as well. See
CHANGELOG.md for behaviour that changed in 3.0.
Development
uv sync # Python 3.14 environment with dev tools
uv run pytest # unit tests
docker compose up -d --wait # Elasticsearch 9.5 on localhost:9200
uv run pytest -m integration # end-to-end tests against it
uv run ruff check . && uv run ruff format .
The corpus test runs the reader over every log Zeek 8.2.2 writes for the PCAP-ATTACK captures, in both TSV and JSON form, and checks the two parse to the same records:
git clone --depth 1 https://github.com/sbousseaden/PCAP-ATTACK .cache/PCAP-ATTACK
scripts/generate_zeek_logs.sh .cache/PCAP-ATTACK .cache/zeek-logs # needs Docker
uv run pytest -m samples
tests/data/ holds a curated subset of that output plus the original 2018
sample conn.log; see the README there for provenance.
Releasing
Bump version in pyproject.toml and __version__ in
parsezeeklogs/__init__.py, note the release in CHANGELOG.md, merge, then
publish a GitHub release tagged v<version>. The Release workflow rebuilds,
checks the tag against the package version, and publishes to PyPI through
trusted publishing.
Thanks
- @geekscrapy: bug fixes and the safe header feature
- @fryguy04: the multi-file JSON example
- @alpiquero: the boolean parsing fix
- @glorello: packaging and column alignment fixes
License
Apache License 2.0. See LICENSE.txt.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file parsezeeklogs-3.0.0.tar.gz.
File metadata
- Download URL: parsezeeklogs-3.0.0.tar.gz
- Upload date:
- Size: 156.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
08c84f21c39233d3beea113853be7708e8a11b46b1b583974df054f8bc2a1579
|
|
| MD5 |
3d7f3ac939f9c8e3099f217e42bbd05a
|
|
| BLAKE2b-256 |
2f544c8a1cdb2f047924248f442c2e1858bfd23557afda055edad1865aacb489
|
File details
Details for the file parsezeeklogs-3.0.0-py3-none-any.whl.
File metadata
- Download URL: parsezeeklogs-3.0.0-py3-none-any.whl
- Upload date:
- Size: 41.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7fbb5622b75ad3acd6e0113cb176166a6d52c6d6b8dcfb876f11ca5594ab0d0b
|
|
| MD5 |
47c99d39f5c48124155bf8b2f16cd306
|
|
| BLAKE2b-256 |
a3a1d8516736d516bf8ab9df3a73d29755c4f06c2e2e9bae8d43ddf33d524884
|