tutorial

zeek_health.py

## Prerequisites - Zeek 5.x or 6.x running on Linux (standalone or cluster mode) - `zeekctl` available on the host - A free account at [vigilmon.online](http...

Prerequisites

  • Zeek 5.x or 6.x running on Linux (standalone or cluster mode)
  • zeekctl available on the host
  • A free account at vigilmon.online

Part 1: Understanding what to monitor in Zeek

Zeek is a passive packet analysis engine. It does not expose an HTTP API, so monitoring happens via:

  1. zeekctl status — checks if the Zeek process is running
  2. Log file freshness — confirms Zeek is writing logs to disk
  3. Log pipeline health — confirms downstream consumers (Kafka, Elasticsearch, Splunk) are receiving data

All three need to be monitored. A Zeek process that is "running" but not writing logs is as bad as a crashed Zeek.


Part 2: Build a Zeek HTTP health endpoint

Create zeek_health.py

import subprocess
import os
import time
from fastapi import FastAPI, Response

app = FastAPI()

ZEEK_LOG_DIR = os.environ.get("ZEEK_LOG_DIR", "/var/log/zeek/current")
MAX_LOG_STALENESS_SEC = int(os.environ.get("ZEEK_LOG_STALENESS_SEC", "120"))


def run_zeekctl(command: str):
    result = subprocess.run(
        ["zeekctl", command],
        capture_output=True,
        text=True,
        timeout=15,
    )
    return result.returncode, result.stdout + result.stderr


@app.get("/health")
def health(response: Response):
    checks = {}

    try:
        rc, output = run_zeekctl("status")
        if rc == 0 and ("running" in output.lower() or "zeek" in output.lower()):
            checks["zeek_process"] = "ok"
        else:
            checks["zeek_process"] = f"error: {output.strip()[:200]}"
    except Exception as e:
        checks["zeek_process"] = f"error: {e}"

    try:
        if os.path.isdir(ZEEK_LOG_DIR):
            log_files = [
                os.path.join(ZEEK_LOG_DIR, f)
                for f in os.listdir(ZEEK_LOG_DIR)
                if f.endswith(".log")
            ]
            if log_files:
                latest_mtime = max(os.path.getmtime(f) for f in log_files)
                age_sec = time.time() - latest_mtime
                checks["log_freshness_sec"] = int(age_sec)
                if age_sec > MAX_LOG_STALENESS_SEC:
                    checks["log_status"] = f"warning: no logs written for {int(age_sec)}s"
                else:
                    checks["log_status"] = "ok"
            else:
                checks["log_status"] = "warning: no log files found"
        else:
            checks["log_status"] = f"error: log directory not found: {ZEEK_LOG_DIR}"
    except Exception as e:
        checks["log_status"] = f"error: {e}"

    conn_log = os.path.join(ZEEK_LOG_DIR, "conn.log")
    try:
        if os.path.exists(conn_log):
            conn_age = time.time() - os.path.getmtime(conn_log)
            checks["conn_log_age_sec"] = int(conn_age)
            checks["conn_log"] = "ok" if conn_age < MAX_LOG_STALENESS_SEC else "stale"
        else:
            checks["conn_log"] = "missing"
    except Exception as e:
        checks["conn_log"] = f"error: {e}"

    process_ok = checks.get("zeek_process") == "ok"
    logs_ok = checks.get("log_status") == "ok"
    healthy = process_ok and logs_ok

    response.status_code = 200 if healthy else 503
    return {"status": "ok" if healthy else "degraded", "checks": checks}

Run the health service

pip install fastapi uvicorn
sudo -u zeek uvicorn zeek_health:app --host 0.0.0.0 --port 8094

Add a systemd unit

# /etc/systemd/system/zeek-health.service
[Unit]
Description=Zeek health endpoint
After=network.target

[Service]
User=zeek
Group=zeek
WorkingDirectory=/opt/zeek-health
ExecStart=/usr/local/bin/uvicorn zeek_health:app --host 0.0.0.0 --port 8094
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable zeek-health
sudo systemctl start zeek-health

Verify the endpoint

curl http://localhost:8094/health

Expected response when Zeek is healthy and writing logs:

{
  "status": "ok",
  "checks": {
    "zeek_process": "ok",
    "log_freshness_sec": 8,
    "log_status": "ok",
    "conn_log_age_sec": 8,
    "conn_log": "ok"
  }
}

Part 3: Set up HTTP monitoring in Vigilmon

Monitor Zeek process and log health

  1. Log in to vigilmon.online and click Add Monitor.
  2. Choose HTTP(S) monitor.
  3. Enter: http://your-zeek-host:8094/health
  4. Set interval to 1 minute.
  5. Add a keyword check: must contain "status":"ok".
  6. Add your alert channel.
  7. Click Save.

The keyword check catches the "status":"degraded" case where Zeek is technically running but not writing logs — which is the most common silent failure mode.

Monitor Zeek in cluster mode

In a Zeek cluster, add per-node monitors:

| Monitor name | URL | Notes | |-------------|-----|-------| | Zeek — Manager node | http://zeek-manager:8094/health | Central process | | Zeek — Logger node | http://zeek-logger:8094/health | Log aggregation | | Zeek — Worker 1 | http://zeek-worker1:8094/health | Packet capture | | Zeek — Worker 2 | http://zeek-worker2:8094/health | Packet capture |


Part 4: Monitor the log pipeline downstream

Zeek logs are typically shipped to Elasticsearch, Splunk, Kafka, or an S3-compatible store. Monitor the downstream pipeline separately.

Elasticsearch index freshness

# elastic_zeek_health.py
import os
import requests
from datetime import datetime, timezone, timedelta
from fastapi import FastAPI, Response

app = FastAPI()

ES_URL = os.environ.get("ELASTICSEARCH_URL", "http://localhost:9200")
MAX_INDEX_AGE_MINUTES = int(os.environ.get("ES_ZEEK_MAX_AGE_MIN", "5"))


@app.get("/health/elasticsearch")
def es_health(response: Response):
    try:
        now = datetime.now(timezone.utc)
        threshold = now - timedelta(minutes=MAX_INDEX_AGE_MINUTES)

        query = {
            "query": {
                "range": {
                    "@timestamp": {
                        "gte": threshold.isoformat(),
                        "lt": now.isoformat()
                    }
                }
            },
            "size": 0
        }

        resp = requests.post(
            f"{ES_URL}/zeek-*/_count",
            json=query,
            timeout=10,
        )
        resp.raise_for_status()
        count = resp.json().get("count", 0)

        if count == 0:
            response.status_code = 503
            return {
                "status": "stale",
                "detail": f"No Zeek documents in Elasticsearch in the last {MAX_INDEX_AGE_MINUTES} minutes",
            }

        return {"status": "ok", "recent_doc_count": count}

    except Exception as e:
        response.status_code = 503
        return {"status": "error", "detail": str(e)}

Add a Vigilmon monitor for this endpoint with keyword "status":"ok" and a 5-minute interval.


Part 5: Docker deployment

If Zeek runs in Docker, mount the log volume:

# docker-compose.yml
version: "3.8"

services:
  zeek:
    image: zeek/zeek:latest
    network_mode: host
    cap_add:
      - NET_ADMIN
      - NET_RAW
    volumes:
      - /etc/zeek:/etc/zeek:ro
      - zeek-logs:/var/log/zeek
    command: ["zeekctl", "start"]

  zeek-health:
    image: python:3.12-slim
    network_mode: host
    command: >
      sh -c "pip install -q fastapi uvicorn &&
             uvicorn zeek_health:app --host 0.0.0.0 --port 8094"
    volumes:
      - ./zeek_health.py:/app/zeek_health.py
      - zeek-logs:/var/log/zeek:ro
    working_dir: /app
    environment:
      ZEEK_LOG_DIR: /var/log/zeek/current
      ZEEK_LOG_STALENESS_SEC: "120"

volumes:
  zeek-logs:

In container deployments, if zeekctl is not available in the health container, drop the zeek_process check and rely solely on log freshness.


Part 6: Webhook alerts for network visibility gaps

A Zeek outage means your security team is flying blind. Configure Vigilmon to trigger high-priority alerts:

// Example: Node.js webhook receiver
import express from 'express';

const app = express();
app.use(express.json());

app.post('/webhook/vigilmon', async (req, res) => {
  const { monitor_name, status, url, checked_at } = req.body;

  if (status === 'down') {
    console.error('[SECURITY] Zeek network monitoring is DOWN', {
      monitor: monitor_name,
      url,
      at: checked_at,
    });

    await createSecurityAlert({
      title: `Zeek DOWN: ${monitor_name}`,
      severity: 'high',
      impact: 'Network logs unavailable — threat hunting and detection impaired',
      since: checked_at,
    });

    await flagLogGap({
      source: 'zeek',
      start: checked_at,
      sensor: monitor_name,
    });

  } else if (status === 'up') {
    console.info('[SECURITY] Zeek network monitoring recovered', { monitor: monitor_name });
    await resolveSecurityAlert(monitor_name);
  }

  res.sendStatus(204);
});

Vigilmon sends this payload on status transitions:

{
  "monitor_id": "mon_zeek01",
  "monitor_name": "Zeek — Core network sensor",
  "status": "down",
  "url": "http://zeek-host.internal:8094/health",
  "checked_at": "2026-07-02T15:30:00Z",
  "response_code": 503,
  "response_time_ms": 2001
}

Part 7: SSL monitoring for Zeek log destinations

If Zeek ships logs to HTTPS endpoints (Elasticsearch, Splunk HEC, S3):

  1. In Vigilmon, click Add Monitor.
  2. Choose SSL monitor.
  3. Enter each HTTPS host that receives Zeek logs:
    • elasticsearch.example.com
    • splunk-hec.example.com
  4. Set alert threshold to 14 days before expiry.
  5. Add your alert channel.

An expired certificate on a log destination causes Zeek's log shipper to fail silently, creating a log gap without any process crash.


Summary

Your Zeek deployment now has layered monitoring:

  1. Process and log freshness endpoint — confirms Zeek is running and actively writing logs, polled every 60 seconds by Vigilmon.
  2. Downstream pipeline monitors — catches failures in Elasticsearch, Splunk, or Kafka that would silently drop Zeek logs even while the process is healthy.
  3. Security-aware webhooks — DOWN events create high-priority security alerts and flag log gaps in the SIEM.
  4. Cluster-level monitoring — one monitor per cluster node so you know exactly which worker or logger went down.
  5. SSL monitors — alerts before certificate expiry breaks log delivery to downstream systems.

Vigilmon handles check scheduling, multi-region polling, and alert routing. When Zeek goes down, your SOC team knows within 60 seconds — and the log gap is documented before the next incident review.


Monitor your Zeek network monitoring infrastructure free at vigilmon.online

#zeek #networksecurity #nsm #infosec #monitoring

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →