Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ docker run --rm --name auditbridge \
ghcr.io/onelrian/auditbridge:<immutable-tag>
```

Replace `<immutable-tag>` with a released application version. Do not use
`latest` for production deployments.
> [!WARNING]
> Replace `<immutable-tag>` with a released application version. Do not use
> `latest` in a production deployment, it moves whenever a new release ships.

## Documentation

Expand All @@ -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).
6 changes: 3 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 6 additions & 4 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
10 changes: 6 additions & 4 deletions docs/INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ docker run --rm --name auditbridge \
ghcr.io/onelrian/auditbridge:<immutable-tag>
```

Replace `<immutable-tag>` with a released application version. Do not use
`latest` in a production deployment.
> [!WARNING]
> Replace `<immutable-tag>` with a released application version. Do not use
> `latest` in a production deployment, it moves whenever a new release ships.

## Docker Compose

Expand All @@ -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, `<chart-version>`
below is a `charts/auditbridge/Chart.yaml` version, not an app image tag:

```bash
helm install auditbridge oci://ghcr.io/onelrian/charts/auditbridge \
Expand Down
3 changes: 3 additions & 0 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
@@ -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`:
Expand Down
4 changes: 3 additions & 1 deletion docs/SINKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ case and underscores:
| `SINK_<NAME>_ADDR` | syslog | Destination `host:port` |
| `SINK_<NAME>_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_<NAME>_HEADERS` directly.
Binary file removed docs/images/grafana_dashboard_example.png
Binary file not shown.
Binary file added docs/images/live-delivery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/live-health-metrics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/live-loki-query.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions examples/local-demo/README.md
Original file line number Diff line number Diff line change
@@ -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
```
38 changes: 38 additions & 0 deletions examples/local-demo/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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: {}
75 changes: 75 additions & 0 deletions examples/local-demo/mock_netbird.py
Original file line number Diff line number Diff line change
@@ -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()
16 changes: 16 additions & 0 deletions examples/local-demo/syslog_receiver.py
Original file line number Diff line number Diff line change
@@ -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()
Loading