diff --git a/.github/workflows/pr-notifications.yml b/.github/workflows/pr-notifications.yml new file mode 100644 index 0000000..40d8766 --- /dev/null +++ b/.github/workflows/pr-notifications.yml @@ -0,0 +1,72 @@ +name: Slack PR Review Notifications + +on: + issue_comment: + types: [created] + pull_request_review: + types: [submitted] + pull_request: + types: [closed] + +jobs: + # Handle @iris cr commands in PR comments + pr-notify: + name: PR Review Notification + # Only run on PR comments that mention the bot + if: | + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(github.event.comment.body, '@iris') + uses: observIQ/gha-workflows/.github/workflows/reusable-slack-pr-notify.yml@main + with: + comment_body: ${{ github.event.comment.body }} + pr_api_url: ${{ github.event.issue.pull_request.url }} + pr_labels: ${{ toJSON(github.event.issue.labels) }} + comment_id: ${{ github.event.comment.id }} + secrets: + SLACK_BOT_TOKEN: ${{ secrets.ORG_IRIS_SLACK_BOT_TOKEN }} + GHA_WORKFLOWS_TOKEN: ${{ secrets.ORG_BINDPLANE_BOT_PR_NOTIFY_TOKEN }} + + # Add emoji reactions to Slack message when PR is reviewed + review-status: + name: Add Review Status Reaction + if: | + github.event_name == 'pull_request_review' && + (github.event.review.state == 'approved' || github.event.review.state == 'changes_requested') + uses: observIQ/gha-workflows/.github/workflows/reusable-slack-pr-review-status.yml@main + with: + pr_number: ${{ github.event.pull_request.number }} + reaction_type: ${{ github.event.review.state }} + secrets: + SLACK_BOT_TOKEN: ${{ secrets.ORG_IRIS_SLACK_BOT_TOKEN }} + GHA_WORKFLOWS_TOKEN: ${{ secrets.ORG_BINDPLANE_BOT_PR_NOTIFY_TOKEN }} + + # Add emoji reaction when PR is merged + pr-merged: + name: Add Merged Reaction + if: | + github.event_name == 'pull_request' && + github.event.action == 'closed' && + github.event.pull_request.merged == true + uses: observIQ/gha-workflows/.github/workflows/reusable-slack-pr-review-status.yml@main + with: + pr_number: ${{ github.event.pull_request.number }} + reaction_type: merged + secrets: + SLACK_BOT_TOKEN: ${{ secrets.ORG_IRIS_SLACK_BOT_TOKEN }} + GHA_WORKFLOWS_TOKEN: ${{ secrets.ORG_BINDPLANE_BOT_PR_NOTIFY_TOKEN }} + + # Add emoji reaction when PR is closed without merging + pr-closed: + name: Add Closed Reaction + if: | + github.event_name == 'pull_request' && + github.event.action == 'closed' && + github.event.pull_request.merged == false + uses: observIQ/gha-workflows/.github/workflows/reusable-slack-pr-review-status.yml@main + with: + pr_number: ${{ github.event.pull_request.number }} + reaction_type: closed + secrets: + SLACK_BOT_TOKEN: ${{ secrets.ORG_IRIS_SLACK_BOT_TOKEN }} + GHA_WORKFLOWS_TOKEN: ${{ secrets.ORG_BINDPLANE_BOT_PR_NOTIFY_TOKEN }} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..69f3fd9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ +# CLAUDE.md + +This file provides guidance to Claude Code when working with the Blitz repository. + +## Repository Overview + +Blitz is an open-source load generation tool for testing OpenTelemetry collectors. It generates synthetic log data in various formats and sends it to configurable destinations. + +## Architecture + +### Generators (`generator/`) + +Each generator creates a specific log format: +- `json/` - Structured JSON logs (supports `default` and `pii` log types) +- `winevt/` - Windows Event logs +- `paloalto/` - Palo Alto firewall logs +- `apache/` - Apache Common Log Format +- `apache_combined/` - Apache Combined Log Format +- `apache_error/` - Apache Error logs +- `nginx/` - NGINX logs +- `postgres/` - PostgreSQL logs +- `kubernetes/` - Kubernetes container logs (CRI-O format) +- `filegen/` - File-based log generation + +### Outputs (`output/`) + +Outputs send generated logs to destinations: +- `stdout/` - Standard output +- `tcp/` - TCP socket +- `udp/` - UDP socket +- `syslog/` - Syslog protocol +- `otlp/` - OpenTelemetry Protocol (gRPC) +- `file/` - File output with rotation + +## Important: Keeping Docker Telemetry Generator in Sync + +When adding a new generator to Blitz, you **MUST** also update the Docker telemetry generator setup: + +### Files to Update + +1. **`docker/docker-compose.telemetry-generator.yml`** + - Add a new service block for the generator following the existing pattern + - Use the `x-blitz-common` anchor for common configuration + - Set appropriate environment variables for the generator type + +2. **`docker/README.md`** + - Add the new generator to the "Included Generators" table + - Update the architecture diagram if needed + +3. **`docs/generator/.md`** + - Create documentation for the new generator + +### Example: Adding a New Generator + +When adding a generator called `syslog-rfc5424`, update docker-compose: + +```yaml + # Syslog RFC5424 Log Generator + blitz-syslog-rfc5424: + <<: *blitz-common + environment: + BLITZ_GENERATOR_TYPE: syslog-rfc5424 + BLITZ_GENERATOR_SYSLOGRFC5424_WORKERS: ${BLITZ_WORKERS:-1} + BLITZ_GENERATOR_SYSLOGRFC5424_RATE: ${BLITZ_RATE:-1s} + BLITZ_OUTPUT_TYPE: otlp-grpc + BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector + BLITZ_OUTPUT_OTLPGRPC_PORT: "4317" +``` + +## PII Generator + +The JSON generator supports a `pii` log type that generates 37 different sensitive data types. When adding new PII types: + +1. Update `internal/generator/logtypes/types.go` - Add field to `PIILogData` struct +2. Update `internal/generator/logtypes/pii.go` - Add generator function and call it in `GeneratePIIData()` +3. Update `generator/json/json.go` - Add field to JSON output in `formatAsJSON()` +4. Update `docs/generator/json.md` - Document the new PII type + +## Common Commands + +```bash +# Build +make build + +# Run tests +make test + +# Run linter +make lint + +# Security scan +make security + +# Generate man pages +make man + +# Generate shell completions +make completion +``` + +## Code Style + +- Use lowercase "Bindplane" (not "BindPlane") in all documentation and comments +- Follow existing patterns for new generators +- Include metrics for new components +- Add tests for new functionality diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..ce055ec --- /dev/null +++ b/docker/README.md @@ -0,0 +1,124 @@ +# Telemetry Generator + +A Docker Compose setup that runs all Blitz log generators simultaneously and sends telemetry to Bindplane via a Bindplane Agent. + +## Architecture + +``` +┌─────────────────┐ +│ 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 + +- Docker and Docker Compose +- Bindplane instance with OpAMP enabled +- Bindplane secret key + +## Quick Start + +```bash +# From the blitz repo root directory +OPAMP_ENDPOINT=wss://your-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 + +| 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 \ +OPAMP_SECRET_KEY=your-secret-key \ +BLITZ_RATE=100ms \ +docker compose -f docker/docker-compose.telemetry-generator.yml up +``` + +**Run with more workers:** +```bash +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 +``` + +**Run in background:** +```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 +``` + +## Included Generators + +| 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 | + +## Running Individual Generators + +To run only specific generators: + +```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 +``` + +## Stopping + +```bash +docker compose -f docker/docker-compose.telemetry-generator.yml down +``` + +## Files + +| File | Description | +|------|-------------| +| `docker-compose.telemetry-generator.yml` | Docker Compose configuration | +| `collector-config.yaml` | Bindplane Agent OTLP receiver configuration | + +## 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/collector-config.yaml b/docker/collector-config.yaml new file mode 100644 index 0000000..f071d70 --- /dev/null +++ b/docker/collector-config.yaml @@ -0,0 +1,38 @@ +# 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.telemetry-generator.yml b/docker/docker-compose.telemetry-generator.yml new file mode 100644 index 0000000..c517d84 --- /dev/null +++ b/docker/docker-compose.telemetry-generator.yml @@ -0,0 +1,164 @@ +# 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/docs/generator/json.md b/docs/generator/json.md index 9e3fac3..a827c49 100644 --- a/docs/generator/json.md +++ b/docs/generator/json.md @@ -18,23 +18,115 @@ The JSON generator creates structured JSON log entries with configurable fields. ### PII Log Type +The PII log type generates logs containing 37 different sensitive data types for comprehensive testing: + ```json { "timestamp": "2024-01-15T10:30:45Z", "level": "INFO", "message": "Customer service request completed", - "user_id": "01234567-89abcdef-01234567-89abcdef", - "iban": "US123456789012345678901234", - "phone": "+1-555-123-4567", - "ssn": "123-45-6789", - "event": "processed transaction", - "action": "approved loan application", + "action": "processed transaction", "status": "successful", - "type": "info", - "detail": "Transaction completed successfully" + + "user_id": "a1b2c3d4e5f67890-1234567890abcdef", + "ssn": "123-45-6789", + "iban": "US12000100001234567890", + "phone": "+1-555-123-4567", + "intl_phone": "+44-555-123-4567", + "email": "john.smith42@gmail.com", + "credit_card": "4532 1234 5678 9012", + "dob": "03/15/1985", + "ipv4": "192.168.1.100", + "ipv6": "2001:db8:85a3:0:0:8a2e:370:7334", + "mac_address": "00:1A:2B:3C:4D:5E", + "street_addr": "123 Main St", + "city_state": "New York, NY", + "zip_code": "10001-1234", + + "passport": "A12345678", + "drivers_license": "CA1234567", + "national_id": "AB123456C", + + "bank_account": "12345678901234", + "routing_number": "021000021", + "crypto_wallet": "0x1234567890abcdef1234567890abcdef12345678", + + "medical_record": "MRN-12345678", + "health_insurance": "BCBS123456789", + + "vin": "1HGBH41JXMN109186", + "license_plate": "ABC-1234", + + "employee_id": "EMP123456", + "student_id": "STU123456789", + + "username": "happy_coder42", + "password_hash": "$2a$10$N9qo8uLOickgx2ZMRZoMy...", + "api_key": "api_EXAMPLE_key_1234567890abcdef", + "aws_access_key": "AKIAIOSFODNN7EXAMPLE", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...", + "jwt_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIx...", + + "gps_coords": "40.712776,-74.005974", + "geohash": "dr5regw3p", + + "full_name": "John Smith", + "mothers_maiden": "Johnson", + "security_answer": "Fluffy" } ``` +## PII Data Types (37 Total) + +| Category | Field | Example | +|----------|-------|---------| +| **Core PII** | | | +| UUID/GUID | `user_id` | `a1b2c3d4e5f67890-1234567890abcdef` | +| SSN | `ssn` | `123-45-6789` | +| IBAN | `iban` | `US12000100001234567890` | +| US Phone | `phone` | `+1-555-123-4567` | +| International Phone | `intl_phone` | `+44-555-123-4567` | +| Email | `email` | `john.smith42@gmail.com` | +| Credit Card | `credit_card` | `4532 1234 5678 9012` | +| Date of Birth | `dob` | `03/15/1985` | +| IPv4 Address | `ipv4` | `192.168.1.100` | +| IPv6 Address | `ipv6` | `2001:db8:85a3:0:0:8a2e:370:7334` | +| MAC Address | `mac_address` | `00:1A:2B:3C:4D:5E` | +| Street Address | `street_addr` | `123 Main St` | +| City, State | `city_state` | `New York, NY` | +| Zip Code | `zip_code` | `10001-1234` | +| **Government IDs** | | | +| Passport | `passport` | `A12345678` | +| Driver's License | `drivers_license` | `CA1234567` | +| National ID | `national_id` | `AB123456C` | +| **Financial** | | | +| Bank Account | `bank_account` | `12345678901234` | +| Routing Number | `routing_number` | `021000021` | +| Crypto Wallet | `crypto_wallet` | `0x1234...` or `bc1q...` | +| **Healthcare** | | | +| Medical Record | `medical_record` | `MRN-12345678` | +| Health Insurance | `health_insurance` | `BCBS123456789` | +| **Vehicle** | | | +| VIN | `vin` | `1HGBH41JXMN109186` | +| License Plate | `license_plate` | `ABC-1234` | +| **Employment** | | | +| Employee ID | `employee_id` | `EMP123456` | +| Student ID | `student_id` | `STU123456789` | +| **Auth/Secrets** | | | +| Username | `username` | `happy_coder42` | +| Password Hash | `password_hash` | `$2a$10$...` (bcrypt) | +| API Key | `api_key` | `api_EXAMPLE_key_123...` | +| AWS Access Key | `aws_access_key` | `AKIAIOSFODNN7EXAMPLE` | +| Private Key | `private_key` | `-----BEGIN RSA PRIVATE KEY-----` | +| JWT Token | `jwt_token` | `eyJhbGciOiJIUzI1NiIs...` | +| **Location** | | | +| GPS Coordinates | `gps_coords` | `40.712776,-74.005974` | +| Geohash | `geohash` | `dr5regw3p` | +| **Personal** | | | +| Full Name | `full_name` | `John Smith` | +| Mother's Maiden | `mothers_maiden` | `Johnson` | +| Security Answer | `security_answer` | `Fluffy` | + ## Configuration | YAML Path | Flag Name | Environment Variable | Default | Description | @@ -61,7 +153,7 @@ generator: generator: type: json json: - workers: 2 + workers: 10 rate: 500ms type: pii ``` @@ -75,4 +167,3 @@ The JSON generator exposes the following metrics: - **`blitz_generator_write_errors_total`** (Counter): Total number of write errors, labeled by `error_type` (`unknown` or `timeout`) All metrics include a `component` label set to `generator_json`. - diff --git a/generator/json/json.go b/generator/json/json.go index f060c71..50dfff9 100644 --- a/generator/json/json.go +++ b/generator/json/json.go @@ -266,9 +266,61 @@ func formatAsJSON(data logtypes.LogData) (output.LogRecord, error) { "timestamp": d.TimestampVal, "level": d.LevelVal, "message": d.MessageVal, - "user_id": d.UserIDVal, - "iban": d.IBANVal, - "phone": d.PhoneVal, + + // 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, } timestamp = d.TimestampVal severity = d.LevelVal @@ -289,9 +341,6 @@ func formatAsJSON(data logtypes.LogData) (output.LogRecord, error) { if d.StatusVal != "" { jsonData.(map[string]any)["status"] = d.StatusVal } - if d.SSNVal != "" { - jsonData.(map[string]any)["ssn"] = d.SSNVal - } default: return output.LogRecord{}, fmt.Errorf("unsupported log data type: %T", data) } diff --git a/internal/generator/logtypes/pii.go b/internal/generator/logtypes/pii.go index 9e2df49..2b94679 100644 --- a/internal/generator/logtypes/pii.go +++ b/internal/generator/logtypes/pii.go @@ -100,7 +100,7 @@ func generateIBAN(r *rand.Rand) string { r.Intn(1000000000000)) } -// generatePhone generates a random phone number +// generatePhone generates a random US phone number func generatePhone(r *rand.Rand) string { return fmt.Sprintf("+1-%03d-%03d-%04d", r.Intn(900)+100, @@ -108,19 +108,388 @@ func generatePhone(r *rand.Rand) string { r.Intn(9000)+1000) } +// generateIntlPhone generates a random international phone number +func generateIntlPhone(r *rand.Rand) string { + countryCodes := []string{"+44", "+49", "+33", "+81", "+86", "+91", "+61", "+55"} + cc := countryCodes[r.Intn(len(countryCodes))] + return fmt.Sprintf("%s-%d-%d-%04d", + cc, + r.Intn(900)+100, + r.Intn(900)+100, + r.Intn(9000)+1000) +} + +// generateEmail generates a random email address +func generateEmail(r *rand.Rand) string { + firstNames := []string{"john", "jane", "bob", "alice", "mike", "sarah", "david", "emma"} + lastNames := []string{"smith", "jones", "wilson", "brown", "taylor", "davis", "miller", "anderson"} + domains := []string{"gmail.com", "yahoo.com", "outlook.com", "company.com", "example.org"} + return fmt.Sprintf("%s.%s%d@%s", + firstNames[r.Intn(len(firstNames))], + lastNames[r.Intn(len(lastNames))], + r.Intn(100), + domains[r.Intn(len(domains))]) +} + +// generateCreditCard generates a random credit card number (Luhn-valid format) +func generateCreditCard(r *rand.Rand) string { + // Generate a 16-digit card number with common prefixes + prefixes := []string{"4", "51", "52", "53", "54", "55", "34", "37"} // Visa, MC, Amex + prefix := prefixes[r.Intn(len(prefixes))] + remaining := 16 - len(prefix) - 1 // -1 for check digit + + number := prefix + for i := 0; i < remaining; i++ { + number += fmt.Sprintf("%d", r.Intn(10)) + } + // Add a random check digit (not Luhn-valid, but looks realistic) + number += fmt.Sprintf("%d", r.Intn(10)) + + // Format with spaces + return fmt.Sprintf("%s %s %s %s", number[0:4], number[4:8], number[8:12], number[12:16]) +} + +// generateDOB generates a random date of birth +func generateDOB(r *rand.Rand) string { + // Generate DOB between 18 and 80 years ago + year := time.Now().Year() - 18 - r.Intn(62) + month := r.Intn(12) + 1 + day := r.Intn(28) + 1 + formats := []string{ + fmt.Sprintf("%02d/%02d/%d", month, day, year), + fmt.Sprintf("%d-%02d-%02d", year, month, day), + fmt.Sprintf("%02d-%02d-%d", month, day, year), + } + return formats[r.Intn(len(formats))] +} + +// generateIPv6 generates a random IPv6 address +func generateIPv6(r *rand.Rand) string { + return fmt.Sprintf("%x:%x:%x:%x:%x:%x:%x:%x", + r.Intn(65536), r.Intn(65536), r.Intn(65536), r.Intn(65536), + r.Intn(65536), r.Intn(65536), r.Intn(65536), r.Intn(65536)) +} + +// generateMAC generates a random MAC address +func generateMAC(r *rand.Rand) string { + return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", + r.Intn(256), r.Intn(256), r.Intn(256), + r.Intn(256), r.Intn(256), r.Intn(256)) +} + +// generateStreetAddress generates a random US street address +func generateStreetAddress(r *rand.Rand) string { + streetNames := []string{"Main", "Oak", "Maple", "Cedar", "Pine", "Elm", "Washington", "Park", "Lake", "Hill"} + streetTypes := []string{"St", "Ave", "Blvd", "Dr", "Ln", "Way", "Rd", "Ct"} + return fmt.Sprintf("%d %s %s", + r.Intn(9999)+1, + streetNames[r.Intn(len(streetNames))], + streetTypes[r.Intn(len(streetTypes))]) +} + +// generateCityState generates a random US city and state +func generateCityState(r *rand.Rand) string { + cities := []struct { + city string + state string + }{ + {"New York", "NY"}, {"Los Angeles", "CA"}, {"Chicago", "IL"}, + {"Houston", "TX"}, {"Phoenix", "AZ"}, {"Philadelphia", "PA"}, + {"San Antonio", "TX"}, {"San Diego", "CA"}, {"Dallas", "TX"}, + {"Austin", "TX"}, {"Seattle", "WA"}, {"Denver", "CO"}, + {"Boston", "MA"}, {"Miami", "FL"}, {"Atlanta", "GA"}, + } + loc := cities[r.Intn(len(cities))] + return fmt.Sprintf("%s, %s", loc.city, loc.state) +} + +// generateZipCode generates a random US zip code +func generateZipCode(r *rand.Rand) string { + if r.Float64() < 0.5 { + return fmt.Sprintf("%05d", r.Intn(100000)) + } + return fmt.Sprintf("%05d-%04d", r.Intn(100000), r.Intn(10000)) +} + +// generatePassport generates a random passport number +func generatePassport(r *rand.Rand) string { + // US passport format: 1 letter + 8 digits, or 9 digits + if r.Float64() < 0.5 { + return fmt.Sprintf("%c%08d", 'A'+r.Intn(26), r.Intn(100000000)) + } + return fmt.Sprintf("%09d", r.Intn(1000000000)) +} + +// generateDriversLicense generates a random US driver's license number +func generateDriversLicense(r *rand.Rand) string { + // Various state formats + states := []string{"CA", "NY", "TX", "FL", "IL"} + state := states[r.Intn(len(states))] + switch state { + case "CA": + return fmt.Sprintf("%c%07d", 'A'+r.Intn(26), r.Intn(10000000)) + case "NY": + return fmt.Sprintf("%03d-%03d-%03d", r.Intn(1000), r.Intn(1000), r.Intn(1000)) + default: + return fmt.Sprintf("%s%08d", state, r.Intn(100000000)) + } +} + +// generateNationalID generates a random national ID (non-US) +func generateNationalID(r *rand.Rand) string { + // Various formats: UK NI, Canadian SIN, etc. + formats := []string{ + fmt.Sprintf("AB%06dC", r.Intn(1000000)), // UK National Insurance + fmt.Sprintf("%03d-%03d-%03d", r.Intn(1000), r.Intn(1000), r.Intn(1000)), // Canadian SIN + fmt.Sprintf("%02d%02d%02d-%05d", r.Intn(100), r.Intn(13), r.Intn(32), r.Intn(100000)), // Various EU + } + return formats[r.Intn(len(formats))] +} + +// generateBankAccount generates a random bank account number +func generateBankAccount(r *rand.Rand) string { + // 8-17 digits typical + length := 8 + r.Intn(10) + account := "" + for i := 0; i < length; i++ { + account += fmt.Sprintf("%d", r.Intn(10)) + } + return account +} + +// generateRoutingNumber generates a random ABA routing number +func generateRoutingNumber(r *rand.Rand) string { + return fmt.Sprintf("%09d", r.Intn(1000000000)) +} + +// generateCryptoWallet generates a random cryptocurrency wallet address +func generateCryptoWallet(r *rand.Rand) string { + // Bitcoin or Ethereum format + if r.Float64() < 0.5 { + // Bitcoin (starts with 1, 3, or bc1) + prefixes := []string{"1", "3", "bc1q"} + prefix := prefixes[r.Intn(len(prefixes))] + chars := "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + addr := prefix + length := 26 + r.Intn(10) + for i := 0; i < length; i++ { + addr += string(chars[r.Intn(len(chars))]) + } + return addr + } + // Ethereum (starts with 0x, 40 hex chars) + return fmt.Sprintf("0x%040x", r.Uint64()) +} + +// generateMedicalRecord generates a random Medical Record Number +func generateMedicalRecord(r *rand.Rand) string { + return fmt.Sprintf("MRN-%08d", r.Intn(100000000)) +} + +// generateHealthInsurance generates a random health insurance ID +func generateHealthInsurance(r *rand.Rand) string { + // Medicare-style or private insurance + if r.Float64() < 0.5 { + return fmt.Sprintf("%d%c%c%c%d", r.Intn(10), 'A'+r.Intn(26), 'A'+r.Intn(26), 'A'+r.Intn(26), r.Intn(10)) + } + return fmt.Sprintf("%s%09d", []string{"BCBS", "UHC", "AETNA", "CIGNA"}[r.Intn(4)], r.Intn(1000000000)) +} + +// generateVIN generates a random Vehicle Identification Number +func generateVIN(r *rand.Rand) string { + // VIN is 17 characters, excludes I, O, Q + chars := "ABCDEFGHJKLMNPRSTUVWXYZ0123456789" + vin := "" + for i := 0; i < 17; i++ { + vin += string(chars[r.Intn(len(chars))]) + } + return vin +} + +// generateLicensePlate generates a random license plate +func generateLicensePlate(r *rand.Rand) string { + formats := []string{ + fmt.Sprintf("%c%c%c-%d%d%d%d", 'A'+r.Intn(26), 'A'+r.Intn(26), 'A'+r.Intn(26), r.Intn(10), r.Intn(10), r.Intn(10), r.Intn(10)), + fmt.Sprintf("%d%c%c%c%d%d%d", r.Intn(10), 'A'+r.Intn(26), 'A'+r.Intn(26), 'A'+r.Intn(26), r.Intn(10), r.Intn(10), r.Intn(10)), + fmt.Sprintf("%c%c%c %d%d%d%d", 'A'+r.Intn(26), 'A'+r.Intn(26), 'A'+r.Intn(26), r.Intn(10), r.Intn(10), r.Intn(10), r.Intn(10)), + } + return formats[r.Intn(len(formats))] +} + +// generateEmployeeID generates a random employee ID +func generateEmployeeID(r *rand.Rand) string { + return fmt.Sprintf("EMP%06d", r.Intn(1000000)) +} + +// generateStudentID generates a random student ID +func generateStudentID(r *rand.Rand) string { + return fmt.Sprintf("STU%09d", r.Intn(1000000000)) +} + +// generateUsername generates a random username +func generateUsername(r *rand.Rand) string { + adjectives := []string{"happy", "quick", "clever", "bright", "swift", "cool", "super", "mega"} + nouns := []string{"user", "coder", "dev", "ninja", "guru", "master", "wizard", "hero"} + return fmt.Sprintf("%s_%s%d", adjectives[r.Intn(len(adjectives))], nouns[r.Intn(len(nouns))], r.Intn(1000)) +} + +// generatePasswordHash generates a random password hash +func generatePasswordHash(r *rand.Rand) string { + // Looks like bcrypt hash + chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./" + hash := "$2a$10$" + for i := 0; i < 53; i++ { + hash += string(chars[r.Intn(len(chars))]) + } + return hash +} + +// generateAPIKey generates a random API key +func generateAPIKey(r *rand.Rand) string { + prefixes := []string{"api_key_", "secret_", "token_", "key_", "apikey_"} + prefix := prefixes[r.Intn(len(prefixes))] + chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + key := prefix + for i := 0; i < 32; i++ { + key += string(chars[r.Intn(len(chars))]) + } + return key +} + +// generateAWSAccessKey generates a random AWS Access Key ID +func generateAWSAccessKey(r *rand.Rand) string { + // AWS Access Key IDs start with AKIA, ABIA, ACCA, or ASIA + prefixes := []string{"AKIA", "ABIA", "ACCA", "ASIA"} + chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + key := prefixes[r.Intn(len(prefixes))] + for i := 0; i < 16; i++ { + key += string(chars[r.Intn(len(chars))]) + } + return key +} + +// generatePrivateKey generates a partial private key representation +func generatePrivateKey(r *rand.Rand) string { + return fmt.Sprintf("-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA%s...[REDACTED]\n-----END RSA PRIVATE KEY-----", + fmt.Sprintf("%016x", r.Uint64())) +} + +// generateJWTToken generates a random JWT token +func generateJWTToken(r *rand.Rand) string { + // Generate fake but realistic-looking JWT + chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + header := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + payload := "" + for i := 0; i < 36; i++ { + payload += string(chars[r.Intn(len(chars))]) + } + signature := "" + for i := 0; i < 43; i++ { + signature += string(chars[r.Intn(len(chars))]) + } + return fmt.Sprintf("%s.%s.%s", header, payload, signature) +} + +// generateGPSCoords generates random GPS coordinates +func generateGPSCoords(r *rand.Rand) string { + lat := -90.0 + r.Float64()*180.0 + long := -180.0 + r.Float64()*360.0 + return fmt.Sprintf("%.6f,%.6f", lat, long) +} + +// generateGeohash generates a random geohash +func generateGeohash(r *rand.Rand) string { + chars := "0123456789bcdefghjkmnpqrstuvwxyz" + hash := "" + length := 6 + r.Intn(6) // 6-12 characters + for i := 0; i < length; i++ { + hash += string(chars[r.Intn(len(chars))]) + } + return hash +} + +// generateFullName generates a random full name +func generateFullName(r *rand.Rand) string { + firstNames := []string{"James", "Mary", "John", "Patricia", "Robert", "Jennifer", "Michael", "Linda", "William", "Elizabeth"} + lastNames := []string{"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"} + return fmt.Sprintf("%s %s", firstNames[r.Intn(len(firstNames))], lastNames[r.Intn(len(lastNames))]) +} + +// generateMothersMaiden generates a random mother's maiden name +func generateMothersMaiden(r *rand.Rand) string { + names := []string{"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore"} + return names[r.Intn(len(names))] +} + +// generateSecurityAnswer generates a random security question answer +func generateSecurityAnswer(r *rand.Rand) string { + answers := []string{"Fluffy", "Oak Street Elementary", "Blue", "Toyota Camry", "New York", "Pizza", "Rover", "Springfield", "1995", "Jennifer"} + return answers[r.Intn(len(answers))] +} + // GeneratePIIData creates structured log data for the PII log type +// Includes all common sensitive data types for comprehensive PII testing func GeneratePIIData() (*PIILogData, error) { r := rand.New(rand.NewSource(time.Now().UnixNano())) // #nosec G404 - userID := generateUserID(r) - iban := generateIBAN(r) - phone := generatePhone(r) - + // Generate all PII fields for every log entry data := &PIILogData{ TimestampVal: time.Now(), - UserIDVal: userID, - IBANVal: iban, - PhoneVal: phone, + + // Core PII + UserIDVal: generateUserID(r), + SSNVal: generateSSN(r), + IBANVal: generateIBAN(r), + PhoneVal: generatePhone(r), + IntlPhoneVal: generateIntlPhone(r), + EmailVal: generateEmail(r), + CreditCardVal: generateCreditCard(r), + DOBVal: generateDOB(r), + IPv4Val: generateIP(r), + IPv6Val: generateIPv6(r), + MACAddressVal: generateMAC(r), + StreetAddrVal: generateStreetAddress(r), + CityStateVal: generateCityState(r), + ZipCodeVal: generateZipCode(r), + + // Government IDs + PassportVal: generatePassport(r), + DriversLicenseVal: generateDriversLicense(r), + NationalIDVal: generateNationalID(r), + + // Financial + BankAccountVal: generateBankAccount(r), + RoutingNumberVal: generateRoutingNumber(r), + CryptoWalletVal: generateCryptoWallet(r), + + // Healthcare + MedicalRecordVal: generateMedicalRecord(r), + HealthInsuranceVal: generateHealthInsurance(r), + + // Vehicle + VINVal: generateVIN(r), + LicensePlateVal: generateLicensePlate(r), + + // Employment/Education + EmployeeIDVal: generateEmployeeID(r), + StudentIDVal: generateStudentID(r), + + // Authentication/Secrets + UsernameVal: generateUsername(r), + PasswordHashVal: generatePasswordHash(r), + APIKeyVal: generateAPIKey(r), + AWSAccessKeyVal: generateAWSAccessKey(r), + PrivateKeyVal: generatePrivateKey(r), + JWTTokenVal: generateJWTToken(r), + + // Location + GPSCoordsVal: generateGPSCoords(r), + GeohashVal: generateGeohash(r), + + // Personal + FullNameVal: generateFullName(r), + MothersMaidenVal: generateMothersMaiden(r), + SecurityAnswerVal: generateSecurityAnswer(r), } if r.Float64() < errorProbability { @@ -143,7 +512,6 @@ func GeneratePIIData() (*PIILogData, error) { data.EventVal = errorMsg data.DetailVal = detail } else { - ssn := generateSSN(r) action := actions[r.Intn(len(actions))] status := statuses[r.Intn(len(statuses))] msg := messages[r.Intn(len(messages))] @@ -153,7 +521,6 @@ func GeneratePIIData() (*PIILogData, error) { data.TypeVal = "info" data.ActionVal = action data.StatusVal = status - data.SSNVal = ssn } return data, nil diff --git a/internal/generator/logtypes/types.go b/internal/generator/logtypes/types.go index f4852b9..a8337c4 100644 --- a/internal/generator/logtypes/types.go +++ b/internal/generator/logtypes/types.go @@ -46,6 +46,7 @@ func (d *DefaultLogData) Message() string { } // PIILogData represents the PII log type with banking/PII fields +// Includes all common sensitive data types for comprehensive PII testing type PIILogData struct { TimestampVal time.Time LevelVal string @@ -55,10 +56,61 @@ type PIILogData struct { TypeVal string ActionVal string StatusVal string - UserIDVal string - SSNVal string - IBANVal string - PhoneVal string + + // Core PII Fields + UserIDVal string // UUID/GUID + SSNVal string // Social Security Number + IBANVal string // International Bank Account Number + PhoneVal string // US Phone Number + IntlPhoneVal string // International Phone Number + EmailVal string // Email Address + CreditCardVal string // Credit Card Number + DOBVal string // Date of Birth + IPv4Val string // IPv4 Address + IPv6Val string // IPv6 Address + MACAddressVal string // MAC Address + StreetAddrVal string // US Street Address + CityStateVal string // US City, State + ZipCodeVal string // US Zip Code + + // Government IDs + PassportVal string // Passport Number + DriversLicenseVal string // Driver's License Number + NationalIDVal string // National ID (non-US) + + // Financial + BankAccountVal string // Bank Account Number + RoutingNumberVal string // ABA Routing Number + CryptoWalletVal string // Cryptocurrency Wallet Address + + // Healthcare + MedicalRecordVal string // Medical Record Number (MRN) + HealthInsuranceVal string // Health Insurance ID + + // Vehicle + VINVal string // Vehicle Identification Number + LicensePlateVal string // License Plate Number + + // Employment/Education + EmployeeIDVal string // Employee ID + StudentIDVal string // Student ID + + // Authentication/Secrets + UsernameVal string // Username + PasswordHashVal string // Password Hash + APIKeyVal string // API Key/Token + AWSAccessKeyVal string // AWS Access Key ID + PrivateKeyVal string // Private Key (partial) + JWTTokenVal string // JWT Token + + // Location + GPSCoordsVal string // GPS Coordinates (lat,long) + GeohashVal string // Geohash + + // Personal + FullNameVal string // Full Name + MothersMaidenVal string // Mother's Maiden Name + SecurityAnswerVal string // Security Question Answer } // Timestamp implements LogData interface