diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cedee39..18d7dd3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,8 @@ helm lint charts/auditbridge ``` The tests start local HTTP and syslog listeners. Run them in an environment that -permits binding loopback ports. +permits binding loopback ports. For a real end-to-end check against a live Loki +and syslog receiver, run [examples/local-demo](examples/local-demo/README.md). ## Changes diff --git a/README.md b/README.md index c508f07..3418a1b 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,9 @@ docker run --rm --name auditbridge \ ghcr.io/onelrian/auditbridge: ``` -Replace `` with a released application version. Do not use -`latest` for production deployments. +> [!WARNING] +> Replace `` with a released application version. Do not use +> `latest` in a production deployment, it moves whenever a new release ships. ## Documentation @@ -47,6 +48,21 @@ AuditBridge serves `/healthz`, `/readyz`, and `/metrics` on `METRICS_PORT` at least one configured sink. See [Operations](docs/OPERATIONS.md) for metric names and troubleshooting. +## Verified + +The screenshots below are real output from a live run: real Loki, a real +syslog receiver, and AuditBridge's actual binary, with only the upstream +NetBird API stubbed to fixed sample data (no live account involved). + +![Live delivery to Loki and Wazuh](docs/images/live-delivery.png) +![Delivered events queried back from Loki](docs/images/live-loki-query.png) +![Live /healthz, /readyz, and /metrics output](docs/images/live-health-metrics.png) + +> [!TIP] +> Don't take the screenshots' word for it: `examples/local-demo/` reproduces +> this exact setup with one `docker compose up`. See +> [examples/local-demo/README.md](examples/local-demo/README.md). + ## License Distributed under the MIT License. See [LICENSE](LICENSE). diff --git a/SECURITY.md b/SECURITY.md index 42fa942..4c8c908 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,9 +2,9 @@ ## Supported versions -Security fixes are applied to the current `main` branch before the first -release. After releases begin, the latest supported release line receives -security fixes. +Security fixes are applied to the latest released version and to `main`. +Older released versions do not receive backported fixes; upgrade to the +latest release to pick up a security fix. ## Reporting a vulnerability diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index df0002a..d815620 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -17,9 +17,11 @@ AuditBridge uses environment variables. A direct secret variable and its | `METRICS_PORT` | `9090` | Health and Prometheus HTTP server port | | `RUST_LOG` | `info` | Rust log filter | -`CURSOR_FILE` must be on durable storage to survive a container or pod -replacement. Without it, AuditBridge reprocesses the account audit history -after each restart. NetBird’s audit endpoint returns the full history and does -not expose server-side paging or cursor parameters. +> [!NOTE] +> `CURSOR_FILE` must be on durable storage (a Docker volume or a Kubernetes +> PVC) to survive a container or pod replacement. Without it, AuditBridge +> reprocesses the account's full audit history after every restart, since +> NetBird's audit endpoint returns the full history and exposes no +> server-side paging or cursor parameters. See [Sinks](SINKS.md) for sink-specific variables. diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index a3270fd..95b086a 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -16,8 +16,9 @@ docker run --rm --name auditbridge \ ghcr.io/onelrian/auditbridge: ``` -Replace `` with a released application version. Do not use -`latest` in a production deployment. +> [!WARNING] +> Replace `` with a released application version. Do not use +> `latest` in a production deployment, it moves whenever a new release ships. ## Docker Compose @@ -44,8 +45,9 @@ volumes: ## Kubernetes and Helm -Use the chart after the corresponding application and chart releases exist. -It expects an existing Secret and does not create credentials: +The chart expects an existing Secret and does not create credentials. Chart +releases are versioned independently from application releases, `` +below is a `charts/auditbridge/Chart.yaml` version, not an app image tag: ```bash helm install auditbridge oci://ghcr.io/onelrian/charts/auditbridge \ diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index a1c5151..3862ea5 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -1,5 +1,8 @@ # Operations +See the [Verified section of the root README](../README.md#verified) for a +real, reproducible end-to-end run of everything on this page. + ## Health and metrics The HTTP server exposes these endpoints on `METRICS_PORT`: diff --git a/docs/SINKS.md b/docs/SINKS.md index 8faaf42..9b03a4e 100644 --- a/docs/SINKS.md +++ b/docs/SINKS.md @@ -29,4 +29,6 @@ case and underscores: | `SINK__ADDR` | syslog | Destination `host:port` | | `SINK__PROTOCOL` | syslog | `tcp` or `udp`, default `tcp` | -Use the `_HEADERS_FILE` form for bearer tokens and API keys. +> [!TIP] +> Use the `_HEADERS_FILE` form for bearer tokens and API keys, mounted from a +> Docker or Kubernetes secret, instead of `SINK__HEADERS` directly. diff --git a/docs/images/grafana_dashboard_example.png b/docs/images/grafana_dashboard_example.png deleted file mode 100644 index 0e4cbe5..0000000 Binary files a/docs/images/grafana_dashboard_example.png and /dev/null differ diff --git a/docs/images/live-delivery.png b/docs/images/live-delivery.png new file mode 100644 index 0000000..2035918 Binary files /dev/null and b/docs/images/live-delivery.png differ diff --git a/docs/images/live-health-metrics.png b/docs/images/live-health-metrics.png new file mode 100644 index 0000000..bd5774f Binary files /dev/null and b/docs/images/live-health-metrics.png differ diff --git a/docs/images/live-loki-query.png b/docs/images/live-loki-query.png new file mode 100644 index 0000000..e8e3741 Binary files /dev/null and b/docs/images/live-loki-query.png differ diff --git a/examples/local-demo/README.md b/examples/local-demo/README.md new file mode 100644 index 0000000..c633172 --- /dev/null +++ b/examples/local-demo/README.md @@ -0,0 +1,40 @@ +# Local demo + +Runs AuditBridge end to end against a real Loki, a real syslog receiver, and +a stubbed NetBird API (`mock_netbird.py` serves fixed sample audit events, no +real NetBird account or credentials are used). This reproduces exactly what +the screenshots in [docs/OPERATIONS.md](../../docs/OPERATIONS.md) show. + +```bash +docker compose up -d --build +``` + +Then verify delivery yourself: + +```bash +# Startup and delivery logs +docker compose logs auditbridge + +# Events actually stored in Loki +curl -s 'http://localhost:3100/loki/api/v1/query_range?query={job="netbird-events"}' | jq + +# Frames received by the Wazuh-equivalent syslog receiver +docker compose logs wazuh-receiver + +# Live health and metrics +curl http://localhost:19090/healthz +curl http://localhost:19090/readyz +curl http://localhost:19090/metrics +``` + +Loki's API isn't exposed to the host by default in this compose file; run +the query from inside the `loki` container instead if you don't want to add +a port mapping: + +```bash +docker compose exec loki wget -qO- 'http://localhost:3100/loki/api/v1/query_range?query={job="netbird-events"}' +``` + +```bash +docker compose down -v +``` diff --git a/examples/local-demo/docker-compose.yml b/examples/local-demo/docker-compose.yml new file mode 100644 index 0000000..4153d2c --- /dev/null +++ b/examples/local-demo/docker-compose.yml @@ -0,0 +1,38 @@ +services: + mock-netbird: + image: python:3.12-slim + volumes: + - ./mock_netbird.py:/mock_netbird.py:ro + command: ["python3", "-u", "/mock_netbird.py"] + networks: [demo] + + wazuh-receiver: + image: python:3.12-slim + volumes: + - ./syslog_receiver.py:/syslog_receiver.py:ro + command: ["python3", "-u", "/syslog_receiver.py"] + networks: [demo] + + loki: + image: grafana/loki:3.2.1 + command: ["-config.file=/etc/loki/local-config.yaml"] + networks: [demo] + + auditbridge: + build: + context: ../.. + environment: + NETBIRD_API_URL: "http://mock-netbird:8080" + NETBIRD_API_TOKEN: "demo-token-not-real" + SINKS: "loki,wazuh" + LOKI_URL: "http://loki:3100" + SINK_WAZUH_ADDR: "wazuh-receiver:1514" + CHECK_INTERVAL: "5" + RUST_LOG: "info" + ports: + - "19090:9090" + depends_on: [mock-netbird, loki, wazuh-receiver] + networks: [demo] + +networks: + demo: {} diff --git a/examples/local-demo/mock_netbird.py b/examples/local-demo/mock_netbird.py new file mode 100644 index 0000000..d310d1e --- /dev/null +++ b/examples/local-demo/mock_netbird.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Stand-in for the real NetBird Management API's /api/events/audit endpoint. +Serves a fixed set of realistic (but fake) audit events so the live test +exercises the real fetch/encode/deliver pipeline end to end. No real +NetBird account or credentials are involved.""" +import http.server +import json +from datetime import datetime, timedelta, timezone + +now = datetime.now(timezone.utc) + + +def ts(seconds_ago): + return (now - timedelta(seconds=seconds_ago)).strftime("%Y-%m-%dT%H:%M:%SZ") + + +EVENTS = [ + { + "id": "evt-1001", + "timestamp": ts(9), + "activity": "Peer added", + "activity_code": "peer.add", + "initiator_id": "user-alice", + "initiator_email": "alice@example.com", + "initiator_name": "Alice Example", + "target_id": "peer-7f3a", + "account_id": "acc-demo-01", + "meta": {"peer_name": "laptop-alice"}, + }, + { + "id": "evt-1002", + "timestamp": ts(5), + "activity": "Group created", + "activity_code": "group.add", + "initiator_id": "user-bob", + "initiator_email": "bob@example.com", + "initiator_name": "Bob Example", + "target_id": "group-eng", + "account_id": "acc-demo-01", + "meta": {"group_name": "engineering"}, + }, + { + "id": "evt-1003", + "timestamp": ts(1), + "activity": "User login", + "activity_code": "user.login", + "initiator_id": "user-alice", + "initiator_email": "alice@example.com", + "initiator_name": "Alice Example", + "target_id": None, + "account_id": "acc-demo-01", + "meta": None, + }, +] + + +class Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + if self.path == "/api/events/audit": + body = json.dumps(EVENTS).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + else: + self.send_response(404) + self.end_headers() + + def log_message(self, fmt, *args): + print(f"[mock-netbird] {self.address_string()} {fmt % args}") + + +if __name__ == "__main__": + http.server.HTTPServer(("0.0.0.0", 8080), Handler).serve_forever() diff --git a/examples/local-demo/syslog_receiver.py b/examples/local-demo/syslog_receiver.py new file mode 100644 index 0000000..3cb3f7c --- /dev/null +++ b/examples/local-demo/syslog_receiver.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +"""Minimal TCP syslog receiver standing in for a Wazuh manager's listener, +to prove the RFC3164 syslog sink actually delivers framed events over the +wire, not just that it compiles.""" +import socketserver + + +class Handler(socketserver.StreamRequestHandler): + def handle(self): + for line in self.rfile: + print(f"[wazuh-receiver] {line.decode().rstrip()}") + + +if __name__ == "__main__": + with socketserver.ThreadingTCPServer(("0.0.0.0", 1514), Handler) as server: + server.serve_forever()