Skip to main content

ParseZeekLogs

Read Zeek network security monitor logs into typed Python records, then do something useful with them: filter them like zeek-cut, convert them to JSON or CSV, drop them into a pandas DataFrame for a threat hunt, or load them into Elasticsearch as Elastic Common Schema documents.

PyPI Build Quality Gate Coverage

pip install parsezeeklogs
from parsezeeklogs import read_zeek

for conn in read_zeek("conn.log"):
    if conn["duration"] and conn["duration"] > 600 and conn["local_resp"] is False:
        print(conn["ts"], conn["id.orig_h"], "->", conn["id.resp_h"], conn["id.resp_p"], conn["service"])

Every value arrives as the right Python type: ports and counts are int, durations and timestamps are float, T/F are bool, sets and vectors are lists, and Zeek's - (unset) is None. No dependencies. Python 3.10+.

Why this exists

ParseBroLogs started in January 2018 after one too many rewrites of the same Bro log parser for one-off analysis scripts. The goals were modest: a lightweight package with no dependencies, a way to pick fields the way bro-cut does, and output that was equally at home in a Jupyter notebook and in a CSV opened by an analyst in Excel. It went on to power the network analysis in the Threat Hunting with Python series, where Bro http.log, ntlm.log and other logs were pulled into pandas to find Nmap scans by their status-code behaviour and to audit administrator logons crossing from a corporate subnet into a process control network.

Version 3.0 is a rewrite for today's stack. Zeek's TSV and JSON log formats are both read by the same code, every Zeek type is converted faithfully, and the output can be Elastic Common Schema documents laid out the way the Filebeat Zeek module produces them. The original 2018 write-up is preserved in docs/blog-2018-parsebrologs.md.

A short tour

Reading logs

from parsezeeklogs import ZeekLog

with ZeekLog("dns.log") as log:
    print(log.path)           # "dns"
    print(log.types["query"]) # "string"
    print(log.fields[:6])     # ['ts', 'uid', 'id.orig_h', 'id.orig_p', 'id.resp_h', 'id.resp_p']
    for rec in log:
        if rec["qtype_name"] == "TXT" and rec["answers"]:
            print(rec["query"], rec["answers"])
    print(log.skipped)        # malformed lines that were reported and skipped

The reader looks at the first line to decide whether it has a TSV log (a #separator header) or a JSON log (LogAscii::use_json=T), so both work without telling it which. Gzip-compressed files and concatenated rotated logs, which repeat their header mid-file, are handled. Options:

Option Effect
fields=[...] keep only these fields, like zeek-cut
safe_headers=True id.orig_h becomes id_orig_h; handy for CSV headers and databases
time_format="iso" emit time values as ISO-8601 strings instead of epoch floats
meta={...} merge a dict into every record, for example the sensor name
on_error=callback called with (line number, text, exception) for each skipped line

Hunting in a notebook

Records are plain dicts, so a DataFrame is one line away. This is the user-agent stacking from part two of the hunting series, against Zeek 8 logs, without any of the manual tab-splitting the original notebook needed:

import pandas as pd
from parsezeeklogs import read_zeek

http = pd.DataFrame(read_zeek("http.log", time_format="datetime"))

# Who is talking HTTP, and with what?
http["user_agent"].fillna("(none)").value_counts().head(10)

# Scanners hit URIs that do not exist: 404s per client per minute
errors = http[http["status_code"] >= 400]
errors.groupby([errors["ts"].dt.floor("min"), "id.orig_h"]).size().sort_values(ascending=False).head()

And the SMB audit from part three, on ntlm.log: privileged accounts authenticating from the corporate subnet into the control network.

ntlm = pd.DataFrame(read_zeek("ntlm.log"))
pivots = ntlm[
    ntlm["id.orig_h"].str.startswith("192.168.5.")
    & ntlm["id.resp_h"].str.startswith("192.168.6.")
    & (ntlm["username"].str.lower() == "administrator")
]
pivots[["ts", "id.orig_h", "id.resp_h", "username", "hostname", "domainname", "success"]]

Converting logs

From the shell:

parsezeeklogs json conn.log dns.log -o zeek.jsonl
parsezeeklogs csv conn.log -f ts,id.orig_h,id.orig_p,id.resp_h,id.resp_p,duration -o conn.csv
parsezeeklogs fields ssl.log        # field name and Zeek type, one per line

From Python, to_json, write_json_lines and write_csv do the same for any iterable of records.

Elastic Common Schema

Add --ecs and the same records come out as ECS documents in the layout the Filebeat Zeek module uses. Common fields become source.*, destination.*, network.*, event.*, dns.*, tls.*, file.* and user.*; the rest stays under zeek.<log>.*; the Zeek uid lands in zeek.session_id and event.id.

parsezeeklogs json conn.log --ecs | head -1 | jq .
{
  "@timestamp": "2020-07-20T22:34:18.733203+00:00",
  "event": {"kind": "event", "category": ["network"], "type": ["connection", "start"],
            "dataset": "zeek.connection", "duration": 31884000, "id": "CJioLj2aEkGc9R3T0b"},
  "source": {"ip": "192.168.56.101", "port": 5353, "bytes": 328, "packets": 4},
  "destination": {"ip": "224.0.0.251", "port": 5353, "bytes": 0, "packets": 0},
  "network": {"transport": "udp", "protocol": "dns", "direction": "internal",
              "bytes": 328, "packets": 4, "community_id": "1:ZtmuRe1iQygjfEmIgxoGU7wxP8E="},
  "related": {"ip": ["192.168.56.101", "224.0.0.251"]},
  "zeek": {"session_id": "CJioLj2aEkGc9R3T0b",
           "connection": {"state": "S0", "state_message": "Connection attempt seen, no reply.",
                          "history": "D", "local_orig": true, "local_resp": true}},
  "ecs": {"version": "9.5.0"}
}

Community ID flow hashes, network.direction, connection state messages, DNS answers, TLS versions and certificate subjects are derived the same way the module's ingest pipelines derive them. Every field is coerced to its ECS or Beats type, so event.duration is an integer of nanoseconds, dns.id is a string, tls.established is a boolean, and array fields are lists. 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.

from parsezeeklogs import read_zeek, to_ecs

for rec in read_zeek("conn.log"):
    doc = to_ecs(rec, "conn", {"observer": {"name": "sensor-1"}})

Loading into Elasticsearch

pip install 'parsezeeklogs[elasticsearch]'
parsezeeklogs elk *.log https://es.example.com:9200 -i zeek-ecs --ecs --create-index --api-key "$ES_API_KEY" --ca-certs ca.pem

--create-index builds the index mapping from the same field table the transform uses, so source.ip is an ip field you can query by CIDR and event.duration is a long you can range over. Without --ecs, documents keep Zeek's field names and gain @timestamp and @path. Authentication options: -u/--user with -p/--password (prompted when omitted), --api-key, --ca-certs, -k/--insecure. Exit status is 1 when any bulk item failed or the cluster could not be reached.

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)

Still on 2.x?

The old iterator is still there and now returns correctly typed data:

from parsezeeklogs import ParseZeekLogs

for line in ParseZeekLogs("conn.log", output_format="json", safe_headers=True):
    print(line)

ParseZeekLogs.batch_to_elk(...) is kept too. CHANGELOG.md lists what changed, including the 2.x bugs that are now fixed: booleans were always true, time and int fields were strings, and unset numeric fields shifted CSV columns.

Command reference

parsezeeklogs json   FILE... [-o OUT] [--ecs] [-f FIELDS] [--safe-headers] [-m JSON] [--time-format iso]
parsezeeklogs csv    FILE... [-o OUT] [-f FIELDS] [--no-header] [--safe-headers] [-m JSON]
parsezeeklogs fields FILE
parsezeeklogs elk    FILE... URL [-i INDEX] [-s BULK] [--ecs] [--create-index] [-m JSON]
                                 [-u USER] [-p PASS] [--api-key KEY] [--ca-certs PEM] [-k] [--timeout S]

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 that 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.

Further reading

Thanks

License

Apache License 2.0. See LICENSE.txt.

Metadata

Release files for parsezeeklogs 3.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for parsezeeklogs 3.0.1
File Size Uploaded Provenance
parsezeeklogs-3.0.1.tar.gz 160.8 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for parsezeeklogs 3.0.1
File Interpreter ABI Platform
parsezeeklogs-3.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 203.7 kB

Release files / parsezeeklogs-3.0.1.tar.gz

Download URL parsezeeklogs-3.0.1.tar.gz
Size 160.8 kB
Tags Source
SHA-256 checksum
How to use checksums
bc9958dd7efc88ea609808af4f9ca6cfd22fcfb6dce223178a694c7841c098c2
BLAKE2b-256 checksum
How to use checksums
65849d4687eb668f1c0da29778406a479a90e3bfd4329bcefdab7646021c1aa9
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / parsezeeklogs-3.0.1-py3-none-any.whl

Download URL parsezeeklogs-3.0.1-py3-none-any.whl
Size 42.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ab50ab566a69ad2cc7a05f279264a7598b112c6530e8391ec8de4aa581682483
BLAKE2b-256 checksum
How to use checksums
47b57109d37391be6928bdb0c4ef096892ca7e96935e70dab53c2555c6866cfd
Upload date
Uploaded using Trusted Publishing?
What is 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}

Security for parsezeeklogs 3.0.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page