mirror of
https://github.com/SpecterOps/Nemesis
synced 2026-06-08 12:36:42 +00:00
79682c8aa9
* Conversion of NoseyParker scanner to Titus - Old `noseyparker_scanner` project removed, new `titus_scaner` Golang project built - Replaced `noseyparker` references with `titus` as appropriate - Bumped various built-in docker image versions (Dapr, Gotenberg, etc.) * Replace ZIP-only extraction with Titus library archive support Replace custom ZIP extraction code with Titus library's enum.ExtractText() to support scanning secrets inside zip, jar, war, ear, apk, ipa, xpi, crx, tar, tar.gz/tgz, and 7z archives. Document formats (xlsx, docx, pdf, etc.) are intentionally excluded since Nemesis handles those via document_conversion. - Add original_path to TitusInput so Titus can dispatch by file extension (MinIO stores files by UUID without extensions) - Add magic-byte fallback detection for ZIP, 7z, gzip, and tar - Preserve git-repo-in-ZIP detection as a special case before archive scan - Rename env vars: DECOMPRESS_ZIPS -> EXTRACT_ARCHIVES, MAX_EXTRACT_SIZE_MB -> EXTRACT_MAX_TOTAL_SIZE_MB, and add EXTRACT_MAX_FILE_SIZE_MB and EXTRACT_MAX_DEPTH - Add unit tests for archive detection and config loading * Integrated (optional) Titus secret validation - Integrated (optional) Titus secret validation that can be enabled * Added Titus rule ID display and ability to disable specific rules - Added Titus rule ID display and ability to disable specific built-in rules (by ID) * `titus_scanner` fixes - One scanner per concurrent file to prevent timeouts - Fix custom rule regexes to prevent timeouts - Exclude a few default run-away rules causing timeouts - Bumped cores for titus scanner to 2 * Enable Vectorscan/Hyperscan acceleration for titus_scanner Switch from the pure-Go regexp2 engine to the Vectorscan (Hyperscan) SIMD-accelerated engine, using a fork with parallelized regexp2 confirmation and content-based deduplication Key changes: - Dockerfile: install libvectorscan-dev/libvectorscan5, build with CGO_ENABLED=1 and -tags vectorscan - go.mod: replace praetorian-inc/titus with HarmJ0y/titus fork that parallelizes the regexp2 confirmation loop and uses content-based dedup (matching portable engine behavior) - Add bounded-concurrency semaphore in handler to prevent OOM from unbounded goroutines while still returning 200 to Dapr immediately - Add content-based deduplication, match count cap, snippet truncation, and payload size logging to prevent oversized publishes - Replace O(N*M) line/column computation with precomputed line index using binary search - Add GOMEMLIMIT and memory limit to container resources * Update README.md * remove `noseyparker_scanner` container * Add EVTX (Windows Event Log) enrichment module Parses .evtx files and extracts security-relevant data: - Markdown summary transform with event counts, timeline, unique accounts/IPs - Downloadable CSV transforms for account changes, group membership changes, explicit credential use (4648), process creation (4688), and task changes - Findings for high-signal events: audit log cleared, new services, admin group changes, account changes, explicit creds, scheduled tasks, PS script blocks - PowerShell 4104 script blocks reassembled and resubmitted to enrichment pipeline as child .ps1 files for full Titus/YARA scanning - Added de-duping for duplicate PS scripts carved from EVTXs (hash-based) * EVTX mods - Power timelining done for SYSTEM - Inbound/outbound auth for SECURITY - Filtering for default system accounts - CSV viewer now can download files, transforms for EVTX transformed to the viewer * Convert titus scanner to Dapr bulk subscribe for batch processing - Add bulk subscribe types to models (BulkMessagePayload, BulkResponse, etc.) - Replace async single-event handler with synchronous HandleBulkEvent - Process batches with bounded concurrency via semaphore + WaitGroup - Handle edge cases: duplicate entryId, empty objectID/entryId, panic recovery - Add BulkMaxMessages and BulkMaxAwaitDurationMs config with clamping - Set app-max-concurrency to 1 (parallelism within work batch via scanner pool) --------- Co-authored-by: Lee Chagolla-Christensen <lee@localhost>
1234 lines
44 KiB
YAML
1234 lines
44 KiB
YAML
name: nemesis
|
|
|
|
networks:
|
|
nemesis:
|
|
driver: bridge
|
|
|
|
volumes:
|
|
grafana_data:
|
|
jaeger_data:
|
|
loki_data:
|
|
minio_data:
|
|
phoenix_data:
|
|
postgres_data:
|
|
prometheus_data:
|
|
rabbitmq_data:
|
|
scheduler_data:
|
|
empty:
|
|
empty-mounted-containers:
|
|
|
|
services:
|
|
########################################
|
|
# Enrichment Services + Dapr sidecars
|
|
########################################
|
|
|
|
web-api:
|
|
image: ghcr.io/specterops/nemesis/web-api:latest
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/healthz"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
environment:
|
|
- APP_ID=web-api
|
|
- DAPR_GRPC_PORT=50001
|
|
- DAPR_HTTP_PORT=3500
|
|
- DEFAULT_EXPIRATION_DAYS=${DEFAULT_EXPIRATION_DAYS:-100}
|
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
volumes:
|
|
- ${MOUNTED_CONTAINER_PATH:-empty-mounted-containers}:/mounted-containers
|
|
logging: &logging-config
|
|
driver: "json-file"
|
|
options: { max-size: "10m", max-file: "3" }
|
|
depends_on:
|
|
postgres: { condition: service_started }
|
|
placement: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.web-api.rule=PathPrefix(`/api`)"
|
|
- "traefik.http.services.web-api.loadbalancer.server.port=8000"
|
|
- "traefik.http.routers.web-api.entrypoints=websecure"
|
|
- "traefik.http.routers.web-api.tls=true"
|
|
- "traefik.http.routers.web-api.middlewares=auth"
|
|
web-api-dapr:
|
|
image: "daprio/daprd:1.16.9"
|
|
command:
|
|
[
|
|
"./daprd",
|
|
"--app-id",
|
|
"web-api",
|
|
"--max-body-size",
|
|
"1Gi",
|
|
"--app-port",
|
|
"8000",
|
|
"--dapr-http-port",
|
|
"3500",
|
|
"--dapr-grpc-port",
|
|
"50001",
|
|
"--placement-host-address",
|
|
"placement:50006",
|
|
"--scheduler-host-address",
|
|
"scheduler:50007",
|
|
"--resources-path",
|
|
"/dapr/components",
|
|
"--log-level",
|
|
"${DAPR_LOG_LEVEL:-info}",
|
|
"--config",
|
|
"/dapr/configuration/config.yaml",
|
|
"--enable-metrics",
|
|
"--dapr-graceful-shutdown-seconds",
|
|
"5",
|
|
]
|
|
volumes:
|
|
- ./infra/dapr/components/pubsub/files.yaml:/dapr/components/files.yaml:ro
|
|
- ./infra/dapr/components/pubsub/workflow_monitor.yaml:/dapr/components/workflow_monitor.yaml:ro
|
|
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
|
|
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
|
|
environment:
|
|
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:?}
|
|
- MINIO_ROOT_USER=${MINIO_ROOT_USER:?}
|
|
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
|
|
- POSTGRES_USER=${POSTGRES_USER:?}
|
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?}
|
|
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
|
|
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
|
- POSTGRES_DB=${POSTGRES_DB:-enrichment}
|
|
- POSTGRES_PARAMETERS=${POSTGRES_PARAMETERS:-sslmode=disable}
|
|
- RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD:?}
|
|
- RABBITMQ_USER=${RABBITMQ_USER:?}
|
|
depends_on:
|
|
web-api: { condition: service_started }
|
|
placement: { condition: service_started }
|
|
scheduler: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
network_mode: "service:web-api"
|
|
|
|
titus-scanner:
|
|
image: ghcr.io/specterops/nemesis/titus-scanner:latest
|
|
depends_on:
|
|
minio: { condition: service_healthy }
|
|
rabbitmq: { condition: service_healthy }
|
|
environment:
|
|
- DAPR_HTTP_PORT=3513
|
|
- MINIO_ACCESS_KEY=${MINIO_ROOT_USER:?}
|
|
- MINIO_BUCKET=files
|
|
- MINIO_ENDPOINT=http://minio:9000
|
|
- MINIO_SECRET_KEY=${MINIO_ROOT_PASSWORD:?}
|
|
- LOG_LEVEL=info
|
|
- SNIPPET_LENGTH=${SNIPPET_LENGTH:-512} # context length around any Titus matches
|
|
- MAX_CONCURRENT_FILES=${TITUS_MAX_CONCURRENT_FILES:-2} # maximum number of concurrent files to scan
|
|
- MAX_MATCHES_PER_FILE=${MAX_MATCHES_PER_FILE:-100} # safety cap on matches per file
|
|
- MAX_FILE_SIZE_MB=${MAX_FILE_SIZE_MB:-200} # maximum file size to scan, in megabytes
|
|
- GOMEMLIMIT=1500MiB # soft memory limit for Go GC to be aggressive before kernel OOM kills us
|
|
- EXTRACT_ARCHIVES=${EXTRACT_ARCHIVES:-true} # extract+scan archive contents (zip, jar, war, ear, apk, tar, tar.gz, 7z)
|
|
- EXTRACT_MAX_FILE_SIZE_MB=${EXTRACT_MAX_FILE_SIZE_MB:-10} # max per-file size within archives (MB)
|
|
- EXTRACT_MAX_TOTAL_SIZE_MB=${EXTRACT_MAX_TOTAL_SIZE_MB:-1000} # total extraction budget per archive (MB)
|
|
- EXTRACT_MAX_DEPTH=${EXTRACT_MAX_DEPTH:-2} # max nesting depth for recursive archives
|
|
- ENABLE_VALIDATION=${ENABLE_VALIDATION:-false} # whether to enable credential validation
|
|
- VALIDATION_WORKERS=${VALIDATION_WORKERS:-4} # concurrent validation workers
|
|
- DISABLED_RULES=${DISABLED_RULES:-np.s3.1,np.s3.2,np.linkedin.3,kingfisher.jira.1} # comma-separated rule IDs to disable (np.s3.x have backtracking-prone patterns that timeout on large files, np.linkedin.3 / kingfisher.jira.1 have a lot of false positives)
|
|
- PUBSUB_NAME=titus # Name of the Dapr pubsub resource
|
|
- OUTPUT_TOPIC=titus_output # Name of the output topic in Dapr
|
|
- BULK_MAX_MESSAGES=${BULK_MAX_MESSAGES:-100} # max messages per bulk subscribe batch
|
|
- BULK_MAX_AWAIT_DURATION_MS=${BULK_MAX_AWAIT_DURATION_MS:-1000} # max wait before delivering partial batch (ms)
|
|
volumes:
|
|
- ./projects/titus_scanner/custom_rules/:/opt/titus:ro
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: "1"
|
|
memory: 2G
|
|
titus-scanner-dapr:
|
|
image: "daprio/daprd:1.16.9"
|
|
command:
|
|
[
|
|
"./daprd",
|
|
"--max-body-size",
|
|
"1Gi",
|
|
"--app-id",
|
|
"titus-scanner",
|
|
"--app-port",
|
|
"8080",
|
|
"--app-protocol",
|
|
"http",
|
|
"--dapr-http-port",
|
|
"3513",
|
|
"--placement-host-address",
|
|
"placement:50006",
|
|
"--resources-path",
|
|
"/dapr/components",
|
|
"--log-level",
|
|
"${DAPR_LOG_LEVEL:-info}",
|
|
"--config",
|
|
"/dapr/configuration/config.yaml",
|
|
"--app-max-concurrency",
|
|
"1",
|
|
"--enable-metrics",
|
|
"--dapr-graceful-shutdown-seconds",
|
|
"5",
|
|
]
|
|
environment:
|
|
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
|
|
volumes:
|
|
- ./infra/dapr/components/pubsub/titus.yaml:/dapr/components/titus.yaml:ro
|
|
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
|
|
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
|
|
depends_on: [titus-scanner, placement]
|
|
network_mode: "service:titus-scanner"
|
|
|
|
dotnet-service:
|
|
image: ghcr.io/specterops/nemesis/dotnet-service:latest
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 30s
|
|
depends_on:
|
|
minio: { condition: service_healthy }
|
|
rabbitmq: { condition: service_healthy }
|
|
environment:
|
|
- DAPR_GRPC_PORT=50014
|
|
- DAPR_PORT=3514
|
|
- MINIO_ACCESS_KEY=${MINIO_ROOT_USER:?}
|
|
- MINIO_BUCKET=files
|
|
- MINIO_ENDPOINT=http://minio:9000
|
|
- MINIO_SECRET_KEY=${MINIO_ROOT_PASSWORD:?}
|
|
- ASPNETCORE_URLS=http://0.0.0.0:5000
|
|
expose:
|
|
- "5000"
|
|
dotnet-service-dapr:
|
|
image: "daprio/daprd:1.16.9"
|
|
command:
|
|
[
|
|
"./daprd",
|
|
"--max-body-size",
|
|
"1Gi",
|
|
"--app-id",
|
|
"dotnet-service",
|
|
"--app-port",
|
|
"5000",
|
|
"--app-protocol",
|
|
"http",
|
|
"--dapr-http-port",
|
|
"3514",
|
|
"--dapr-grpc-port",
|
|
"50014",
|
|
"--placement-host-address",
|
|
"placement:50006",
|
|
"--resources-path",
|
|
"/dapr/components",
|
|
"--log-level",
|
|
"${DAPR_LOG_LEVEL:-info}",
|
|
"--config",
|
|
"/dapr/configuration/config.yaml",
|
|
"--app-max-concurrency",
|
|
"1",
|
|
"--enable-metrics",
|
|
"--dapr-graceful-shutdown-seconds",
|
|
"5",
|
|
]
|
|
environment:
|
|
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
|
|
volumes:
|
|
- ./infra/dapr/components/pubsub/dotnet.yaml:/dapr/components/dotnet.yaml:ro
|
|
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
|
|
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
|
|
depends_on: [dotnet-service, placement]
|
|
network_mode: "service:dotnet-service"
|
|
|
|
file-enrichment: &file-enrichment-template
|
|
image: ghcr.io/specterops/nemesis/file-enrichment:latest
|
|
healthcheck: &file-enrichment-healthcheck
|
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8001/healthz"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
volumes: []
|
|
# Uncomment the following line to use custom YARA rules (*.yar/*.yara files)
|
|
# - ./libs/file_enrichment_modules/yara_rules/prod/:/yara_rules/:ro
|
|
environment: &file-enrichment-environment
|
|
ENABLE_PII_DETECTION: ${ENABLE_PII_DETECTION:-false}
|
|
PII_DETECTION_THRESHOLD: ${PII_DETECTION_THRESHOLD:-0.7}
|
|
APP_ID: file-enrichment
|
|
DAPR_GRPC_PORT: 50003
|
|
DAPR_HTTP_PORT: 3503
|
|
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
|
MAX_PARALLEL_WORKFLOWS: ${ENRICHMENT_MAX_PARALLEL_WORKFLOWS:-5}
|
|
MAX_WORKFLOW_EXECUTION_TIME: ${MAX_WORKFLOW_EXECUTION_TIME:-300}
|
|
NEMESIS_MONITORING: ${NEMESIS_MONITORING:-disabled}
|
|
NEMESIS_URL: ${NEMESIS_URL:?}
|
|
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector:4317
|
|
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_INSECURE: true
|
|
# PYTHONASYNCIOTHREADPOOLSIZE: 100 # Increase thread pool for asyncio.to_thread()
|
|
RIGGING_GENERATOR_CREDENTIALS: ${RIGGING_GENERATOR_CREDENTIALS:-}
|
|
RIGGING_GENERATOR_SUMMARY: ${RIGGING_GENERATOR_SUMMARY:-}
|
|
RIGGING_GENERATOR_TRIAGE: ${RIGGING_GENERATOR_TRIAGE:-}
|
|
WORKFLOW_RUNTIME_LOG_LEVEL: ${WORKFLOW_RUNTIME_LOG_LEVEL:-WARNING}
|
|
WORKFLOW_CLIENT_LOG_LEVEL: ${WORKFLOW_CLIENT_LOG_LEVEL:-WARNING}
|
|
logging: *logging-config
|
|
depends_on: &file-enrichment-depends-on
|
|
postgres: { condition: service_started }
|
|
placement: { condition: service_started }
|
|
scheduler: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
file-enrichment-dapr: &file-enrichment-dapr-template
|
|
image: "daprio/daprd:1.16.9"
|
|
command:
|
|
[
|
|
"./daprd",
|
|
"--max-body-size",
|
|
"1Gi",
|
|
"--app-id",
|
|
"file-enrichment",
|
|
"--app-port",
|
|
"8001",
|
|
"--dapr-http-port",
|
|
"3503",
|
|
"--dapr-grpc-port",
|
|
"50003",
|
|
"--placement-host-address",
|
|
"placement:50006",
|
|
"--scheduler-host-address",
|
|
"scheduler:50007",
|
|
"--log-level",
|
|
"${DAPR_LOG_LEVEL:-warn}",
|
|
"--resources-path",
|
|
"/dapr/components",
|
|
"--config",
|
|
"/dapr/configuration/file_enrichment_monitoring_${NEMESIS_MONITORING:-disabled}.yaml",
|
|
"--enable-metrics",
|
|
"--dapr-graceful-shutdown-seconds",
|
|
"5",
|
|
]
|
|
volumes: &file-enrichment-dapr-volumes
|
|
- ./infra/dapr/components/pubsub/alerting.yaml:/dapr/components/alerting.yaml:ro
|
|
- ./infra/dapr/components/pubsub/document_conversion.yaml:/dapr/components/document_conversion.yaml:ro
|
|
- ./infra/dapr/components/pubsub/dotnet.yaml:/dapr/components/dotnet.yaml:ro
|
|
- ./infra/dapr/components/pubsub/dpapi.yaml:/dapr/components/dpapi.yaml:ro
|
|
- ./infra/dapr/components/pubsub/files.yaml:/dapr/components/files.yaml:ro
|
|
- ./infra/dapr/components/pubsub/titus.yaml:/dapr/components/titus.yaml:ro
|
|
- ./infra/dapr/components/pubsub/workflow_monitor.yaml:/dapr/components/workflow_monitor.yaml:ro
|
|
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
|
|
- ./infra/dapr/components/workflowstate.yaml:/dapr/components/workflowstate.yaml:ro
|
|
- ./infra/dapr/configuration/:/dapr/configuration/
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:?}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
|
|
POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
|
|
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
|
|
POSTGRES_DB: ${POSTGRES_DB:-enrichment}
|
|
POSTGRES_PARAMETERS: ${POSTGRES_PARAMETERS:-sslmode=disable}
|
|
RABBITMQ_CONNECTION_STRING: amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
|
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?}
|
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?}
|
|
MAX_PARALLEL_WORKFLOWS: ${MAX_PARALLEL_WORKFLOWS:-5}
|
|
depends_on: &file-enrichment-dapr-depends-on
|
|
file-enrichment: { condition: service_started }
|
|
placement: { condition: service_started }
|
|
scheduler: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
network_mode: "service:file-enrichment"
|
|
|
|
# file-enrichment-1:
|
|
# <<: *file-enrichment-template
|
|
# environment:
|
|
# <<: *file-enrichment-environment
|
|
# APP_ID: file-enrichment-1
|
|
# file-enrichment-1-dapr:
|
|
# <<: *file-enrichment-dapr-template
|
|
# depends_on:
|
|
# file-enrichment-1: { condition: service_started }
|
|
# placement: { condition: service_started }
|
|
# scheduler: { condition: service_started }
|
|
# rabbitmq: { condition: service_healthy }
|
|
# network_mode: "service:file-enrichment-1"
|
|
|
|
# file-enrichment-2:
|
|
# <<: *file-enrichment-template
|
|
# environment:
|
|
# <<: *file-enrichment-environment
|
|
# APP_ID: file-enrichment-2
|
|
# file-enrichment-2-dapr:
|
|
# <<: *file-enrichment-dapr-template
|
|
# depends_on:
|
|
# file-enrichment-2: { condition: service_started }
|
|
# placement: { condition: service_started }
|
|
# scheduler: { condition: service_started }
|
|
# rabbitmq: { condition: service_healthy }
|
|
# network_mode: "service:file-enrichment-2"
|
|
|
|
# file-enrichment-3:
|
|
# <<: *file-enrichment-template
|
|
# environment:
|
|
# <<: *file-enrichment-environment
|
|
# APP_ID: file-enrichment-3
|
|
# file-enrichment-3-dapr:
|
|
# <<: *file-enrichment-dapr-template
|
|
# depends_on:
|
|
# file-enrichment-3: { condition: service_started }
|
|
# placement: { condition: service_started }
|
|
# scheduler: { condition: service_started }
|
|
# rabbitmq: { condition: service_healthy }
|
|
# network_mode: "service:file-enrichment-3"
|
|
|
|
frontend:
|
|
image: ghcr.io/specterops/nemesis/frontend:latest
|
|
depends_on: [postgres, hasura]
|
|
environment:
|
|
- HASURA_ADMIN_SECRET=${HASURA_ADMIN_SECRET:-pass456}
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"]
|
|
interval: 10s
|
|
timeout: 10s
|
|
retries: 3
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.frontend.rule=PathPrefix(`/`)"
|
|
- "traefik.http.services.frontend.loadbalancer.server.port=3000"
|
|
- "traefik.http.routers.frontend.entrypoints=websecure"
|
|
- "traefik.http.routers.frontend.tls=true"
|
|
- "traefik.http.routers.frontend.middlewares=auth"
|
|
|
|
jupyter:
|
|
profiles: ["jupyter"]
|
|
image: ghcr.io/specterops/nemesis/jupyter:latest
|
|
depends_on: [postgres, hasura]
|
|
environment:
|
|
- HASURA_ADMIN_SECRET=${HASURA_ADMIN_SECRET:-pass456}
|
|
- HASURA_GRAPHQL_URL=http://hasura:8080/v1/graphql
|
|
- JUPYTER_PASSWORD=${JUPYTER_PASSWORD:-}
|
|
volumes:
|
|
- "./projects/jupyter/notebooks:/home/jovyan/work"
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:8888/jupyter/api"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.jupyter.rule=PathPrefix(`/jupyter`)"
|
|
- "traefik.http.services.jupyter.loadbalancer.server.port=8888"
|
|
- "traefik.http.routers.jupyter.entrypoints=websecure"
|
|
- "traefik.http.routers.jupyter.tls=true"
|
|
- "traefik.http.routers.jupyter.middlewares=auth"
|
|
|
|
alerting:
|
|
image: ghcr.io/specterops/nemesis/alerting:latest
|
|
environment:
|
|
- DAPR_HTTP_PORT=3508
|
|
- DAPR_GRPC_PORT=50008
|
|
- APPRISE_URLS=${APPRISE_URLS:-}
|
|
- NEMESIS_URL=${NEMESIS_URL:?}
|
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
logging: *logging-config
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
placement: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
hasura: { condition: service_healthy }
|
|
healthcheck: &healthcheck-python-svc
|
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/healthz"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 20s
|
|
alerting-dapr:
|
|
image: "daprio/daprd:1.16.9"
|
|
command:
|
|
[
|
|
"./daprd",
|
|
"--max-body-size",
|
|
"1Gi",
|
|
"--app-id",
|
|
"alerting",
|
|
"--app-port",
|
|
"8000",
|
|
"--dapr-http-port",
|
|
"3508",
|
|
"--dapr-grpc-port",
|
|
"50008",
|
|
"--placement-host-address",
|
|
"placement:50006",
|
|
"--scheduler-host-address",
|
|
"scheduler:50007",
|
|
"--resources-path",
|
|
"/dapr/components",
|
|
"--log-level",
|
|
"${DAPR_LOG_LEVEL:-info}",
|
|
"--config",
|
|
"/dapr/configuration/config.yaml",
|
|
"--enable-metrics",
|
|
"--dapr-graceful-shutdown-seconds",
|
|
"5",
|
|
]
|
|
volumes:
|
|
- ./infra/dapr/components/pubsub/alerting.yaml:/dapr/components/alerting.yaml:ro
|
|
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
|
|
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
|
|
environment:
|
|
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
|
|
- HASURA_ADMIN_SECRET=${HASURA_ADMIN_SECRET:-pass456}
|
|
depends_on:
|
|
alerting: { condition: service_started }
|
|
placement: { condition: service_started }
|
|
scheduler: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
network_mode: "service:alerting"
|
|
|
|
housekeeping:
|
|
image: ghcr.io/specterops/nemesis/housekeeping:latest
|
|
environment:
|
|
- APP_ID=housekeeping
|
|
- CLEANUP_SCHEDULE=*/3 * * * * # Every 3 minutes
|
|
- DAPR_GRPC_PORT=50011
|
|
- DAPR_HTTP_PORT=3510
|
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
logging: *logging-config
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
placement: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
healthcheck: *healthcheck-python-svc
|
|
housekeeping-dapr:
|
|
image: "daprio/daprd:1.16.9"
|
|
command:
|
|
[
|
|
"./daprd",
|
|
"--max-body-size",
|
|
"1Gi",
|
|
"--app-id",
|
|
"housekeeping",
|
|
"--app-port",
|
|
"8000",
|
|
"--dapr-http-port",
|
|
"3510",
|
|
"--dapr-grpc-port",
|
|
"50011",
|
|
"--placement-host-address",
|
|
"placement:50006",
|
|
"--scheduler-host-address",
|
|
"scheduler:50007",
|
|
"--resources-path",
|
|
"/dapr/components",
|
|
"--log-level",
|
|
"${DAPR_LOG_LEVEL:-info}",
|
|
"--config",
|
|
"/dapr/configuration/config.yaml",
|
|
"--enable-metrics",
|
|
"--dapr-graceful-shutdown-seconds",
|
|
"5",
|
|
]
|
|
volumes:
|
|
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
|
|
- ./infra/dapr/components/workflowstate.yaml:/dapr/components/workflowstate.yaml:ro
|
|
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
|
|
environment:
|
|
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:?}
|
|
- MINIO_ROOT_USER=${MINIO_ROOT_USER:?}
|
|
- POSTGRES_USER=${POSTGRES_USER:?}
|
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?}
|
|
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
|
|
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
|
- POSTGRES_DB=${POSTGRES_DB:-enrichment}
|
|
- POSTGRES_PARAMETERS=${POSTGRES_PARAMETERS:-sslmode=disable}
|
|
depends_on:
|
|
housekeeping: { condition: service_started }
|
|
placement: { condition: service_started }
|
|
scheduler: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
network_mode: "service:housekeeping"
|
|
|
|
document-conversion:
|
|
image: ghcr.io/specterops/nemesis/document-conversion:latest
|
|
volumes:
|
|
- ./infra/tika/tika-config${TIKA_USE_OCR:+-tesseract}.xml:/tika-config.xml:ro
|
|
environment:
|
|
- APP_ID=document-conversion
|
|
- DAPR_GRPC_PORT=50002
|
|
- DAPR_HTTP_PORT=3501
|
|
- TIKA_CONFIG=/tika-config.xml
|
|
# If you want to have additional language packs supported (see https://github.com/tesseract-ocr/tessdata for a full list):
|
|
# $ export TIKA_OCR_LANGUAGES="eng chi_sim chi_tra jpn rus deu spa"
|
|
# To change the default:
|
|
# - TIKA_OCR_LANGUAGES=${TIKA_OCR_LANGUAGES:-eng chi_sim chi_tra jpn rus deu spa}
|
|
# Note: each package installed will increase the image size!
|
|
- TIKA_OCR_LANGUAGES=${TIKA_OCR_LANGUAGES:-eng}
|
|
- MAX_PARALLEL_WORKFLOWS=${DOCUMENTCONVERSION_WORKERS:-5}
|
|
- MAX_WORKFLOW_EXECUTION_TIME=${MAX_WORKFLOW_EXECUTION_TIME:-300}
|
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
- OMP_THREAD_LIMIT=1 # Limit the number of tesseract instances
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/healthz"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
logging: *logging-config
|
|
depends_on:
|
|
postgres: { condition: service_started }
|
|
placement: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: "2"
|
|
document-conversion-dapr:
|
|
image: "daprio/daprd:1.16.9"
|
|
command:
|
|
[
|
|
"./daprd",
|
|
"--max-body-size",
|
|
"1Gi",
|
|
"--app-id",
|
|
"document-conversion",
|
|
"--app-port",
|
|
"8000",
|
|
"--dapr-http-port",
|
|
"3501",
|
|
"--dapr-grpc-port",
|
|
"50002",
|
|
"--placement-host-address",
|
|
"placement:50006",
|
|
"--scheduler-host-address",
|
|
"scheduler:50007",
|
|
"--resources-path",
|
|
"/dapr/components",
|
|
"--log-level",
|
|
"${DAPR_LOG_LEVEL:-info}",
|
|
"--config",
|
|
"/dapr/configuration/document_conversion_monitoring_${NEMESIS_MONITORING:-disabled}.yaml",
|
|
"--enable-metrics",
|
|
"--dapr-graceful-shutdown-seconds",
|
|
"5",
|
|
]
|
|
volumes:
|
|
- ./infra/dapr/components/pubsub/document_conversion.yaml:/dapr/components/document_conversion.yaml:ro
|
|
- ./infra/dapr/components/pubsub/files.yaml:/dapr/components/files.yaml:ro
|
|
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
|
|
- ./infra/dapr/components/workflowstate.yaml:/dapr/components/workflowstate.yaml:ro
|
|
- ./infra/dapr/configuration/:/dapr/configuration/
|
|
environment:
|
|
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:?}
|
|
- MINIO_ROOT_USER=${MINIO_ROOT_USER:?}
|
|
- POSTGRES_USER=${POSTGRES_USER:?}
|
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?}
|
|
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
|
|
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
|
- POSTGRES_DB=${POSTGRES_DB:-enrichment}
|
|
- POSTGRES_PARAMETERS=${POSTGRES_PARAMETERS:-sslmode=disable}
|
|
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
|
|
depends_on:
|
|
document-conversion: { condition: service_started }
|
|
placement: { condition: service_started }
|
|
scheduler: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
network_mode: "service:document-conversion"
|
|
|
|
gotenberg:
|
|
#image: gotenberg/gotenberg:8.21.1
|
|
image: gotenberg/gotenberg:8.27.0
|
|
command:
|
|
[
|
|
"gotenberg",
|
|
"--api-timeout=180s",
|
|
"--libreoffice-restart-after=5",
|
|
"--libreoffice-auto-start=true",
|
|
"--prometheus-collect-interval=10s",
|
|
]
|
|
environment: { DISABLE_GOOGLE_CHROME: "1" }
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-sSf", "http://localhost:3000/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
gotenberg-dapr:
|
|
image: "daprio/daprd:1.16.9"
|
|
command:
|
|
[
|
|
"./daprd",
|
|
"--max-body-size",
|
|
"1Gi",
|
|
"--app-id",
|
|
"gotenberg",
|
|
"--app-port",
|
|
"3000",
|
|
"--dapr-http-port",
|
|
"3505",
|
|
"--dapr-grpc-port",
|
|
"50005",
|
|
"--placement-host-address",
|
|
"placement:50006",
|
|
"--scheduler-host-address",
|
|
"scheduler:50007",
|
|
"--resources-path",
|
|
"/dapr/components",
|
|
"--log-level",
|
|
"${DAPR_LOG_LEVEL:-info}",
|
|
"--config",
|
|
"/dapr/configuration/config.yaml",
|
|
"--enable-metrics",
|
|
"--dapr-graceful-shutdown-seconds",
|
|
"5",
|
|
]
|
|
volumes:
|
|
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
|
|
- empty:/dapr/components:ro
|
|
|
|
depends_on:
|
|
gotenberg: { condition: service_started }
|
|
placement: { condition: service_started }
|
|
scheduler: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
network_mode: "service:gotenberg"
|
|
|
|
############################
|
|
# Infrastructure Services
|
|
############################
|
|
minio:
|
|
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
|
|
environment:
|
|
MINIO_PROMETHEUS_AUTH_TYPE: public
|
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?}
|
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?}
|
|
volumes:
|
|
- minio_data:/data
|
|
command: server /data --console-address ":9001" --address ":9000"
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
|
interval: 30s
|
|
timeout: 20s
|
|
retries: 3
|
|
|
|
minio-init:
|
|
image: minio/mc:RELEASE.2025-08-13T08-35-41Z
|
|
depends_on: [minio]
|
|
environment:
|
|
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
|
|
- MINIO_ROOT_USER=${MINIO_ROOT_USER}
|
|
entrypoint:
|
|
[
|
|
"/bin/sh",
|
|
"-c",
|
|
"sleep 1; until mc alias set minio http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}; do echo 'Minio is not online. Waiting for it to start...'; sleep 2; done && mc mb minio/loki-data --ignore-existing && mc mb minio/files --ignore-existing",
|
|
]
|
|
restart: "no"
|
|
|
|
rabbitmq:
|
|
# image: rabbitmq:4.2.0-management
|
|
image: rabbitmq:4.2.4-management
|
|
hostname: rabbitmq-node # have to do this for persistence reasons
|
|
environment:
|
|
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD:?}
|
|
- RABBITMQ_DEFAULT_USER=${RABBITMQ_USER:?}
|
|
- RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS=-rabbitmq_management path_prefix "/rabbitmq"
|
|
- RABBITMQ_NODENAME=rabbit@rabbitmq-node # have to do this for persistence reasons
|
|
volumes:
|
|
- rabbitmq_data:/var/lib/rabbitmq
|
|
- ./infra/rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
|
|
- ./infra/rabbitmq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
|
|
healthcheck:
|
|
test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
|
|
interval: 10s
|
|
timeout: 10s
|
|
retries: 5
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.rabbitmq-ui.entrypoints=websecure"
|
|
- "traefik.http.routers.rabbitmq-ui.tls=true"
|
|
- "traefik.http.services.rabbitmq-ui.loadbalancer.server.port=15672"
|
|
- "traefik.http.routers.rabbitmq-ui.rule=PathPrefix(`/rabbitmq`)"
|
|
|
|
postgres:
|
|
image: postgres:18.1
|
|
command:
|
|
[
|
|
"postgres",
|
|
"-c",
|
|
"max_connections=200",
|
|
"-c",
|
|
"shared_buffers=1GB",
|
|
"-c",
|
|
"shared_preload_libraries=pg_stat_statements",
|
|
"-c",
|
|
"pg_stat_statements.track=all",
|
|
"-c",
|
|
"tcp_keepalives_idle=5",
|
|
"-c",
|
|
"tcp_keepalives_interval=2",
|
|
"-c",
|
|
"tcp_keepalives_count=3",
|
|
]
|
|
# ports:
|
|
# - "${POSTGRES_EXTERNAL_PORT:-}5432"
|
|
environment:
|
|
POSTGRES_DB: ${POSTGRES_DB:-enrichment}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
|
|
POSTGRES_USER: ${POSTGRES_USER:?}
|
|
volumes:
|
|
- ./infra/postgres:/docker-entrypoint-initdb.d:ro
|
|
- postgres_data:/var/lib/postgresql
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d enrichment"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
postgres-exporter:
|
|
profiles: ["monitoring"]
|
|
image: prometheuscommunity/postgres-exporter:v0.18.1
|
|
environment:
|
|
DATA_SOURCE_NAME: "postgresql://${POSTGRES_USER:?}:${POSTGRES_PASSWORD:?}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-enrichment}?${POSTGRES_PARAMETERS:-sslmode=disable}"
|
|
volumes:
|
|
- ./infra/postgres-exporter/postgres_exporter.yml:/postgres_exporter.yml:ro
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
|
|
hasura:
|
|
#image: hasura/graphql-engine:v2.48.6.cli-migrations-v2
|
|
image: hasura/graphql-engine:v2.48.12.cli-migrations-v2
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
environment:
|
|
HASURA_GRAPHQL_ADMIN_SECRET: "${HASURA_ADMIN_SECRET:-pass456}"
|
|
HASURA_GRAPHQL_BASE_PATH: "/hasura"
|
|
HASURA_GRAPHQL_DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-enrichment}
|
|
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
|
|
HASURA_GRAPHQL_ENABLE_METADATA_SYNC: "true"
|
|
HASURA_GRAPHQL_ENABLE_TELEMETRY: "false"
|
|
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
|
|
HASURA_GRAPHQL_LOG_LEVEL: "warn"
|
|
HASURA_GRAPHQL_METADATA_DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-enrichment}
|
|
HASURA_GRAPHQL_METADATA_DIR: /hasura-metadata
|
|
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous
|
|
volumes:
|
|
- ./infra/hasura/metadata:/hasura-metadata:ro
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.hasura.rule=PathPrefix(`/hasura`)"
|
|
- "traefik.http.services.hasura.loadbalancer.server.port=8080"
|
|
- "traefik.http.routers.hasura.middlewares=graphql-stripprefix"
|
|
- "traefik.http.middlewares.graphql-stripprefix.stripprefix.prefixes=/hasura"
|
|
- "traefik.http.routers.hasura.entrypoints=websecure"
|
|
- "traefik.http.routers.hasura.tls=true"
|
|
|
|
traefik:
|
|
# image: traefik:v3.6.1
|
|
image: traefik:v3.6.9
|
|
command:
|
|
- "--api.insecure=true"
|
|
# - "--log.level=DEBUG"
|
|
- "--log.level=WARN"
|
|
- "--providers.docker=true"
|
|
- "--providers.docker.exposedbydefault=false"
|
|
- "--providers.file=true"
|
|
- "--providers.file.directory=/config"
|
|
- "--providers.file.watch=true"
|
|
- "--entrypoints.web.address=:80"
|
|
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
|
|
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
|
|
- "--entrypoints.websecure.address=:443"
|
|
- "--metrics.prometheus=true"
|
|
- "--metrics.prometheus.addrouterslabels=true"
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.middlewares.auth.basicauth.users=${BASIC_AUTH_USERS:-n:$$apr1$$RbnOjtKr$$S706SPJ3KzCKIref.gRFT.}"
|
|
- "traefik.http.routers.dashboard.tls=true"
|
|
environment:
|
|
- NEMESIS_PORT=${NEMESIS_PORT:-7443}
|
|
ports: ["${NEMESIS_PORT:-7443}:443"]
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
- ./auth:/auth:ro
|
|
- ./infra/traefik/certs:/certs:ro
|
|
- ./infra/traefik/config:/config:ro
|
|
|
|
placement:
|
|
image: "daprio/dapr:1.16.9"
|
|
command:
|
|
[
|
|
"./placement",
|
|
"-port",
|
|
"50006",
|
|
"--enable-metrics",
|
|
"--log-level",
|
|
"${DAPR_LOG_LEVEL:-info}",
|
|
]
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
|
|
scheduler-init:
|
|
image: busybox:1.37.0
|
|
user: "0:0"
|
|
command: >
|
|
sh -c "
|
|
mkdir -p /data/default-dapr-scheduler-server-0 &&
|
|
chown -R 65532:65532 /data &&
|
|
chmod -R 755 /data
|
|
"
|
|
volumes:
|
|
- scheduler_data:/data
|
|
restart: "no"
|
|
|
|
scheduler:
|
|
image: "daprio/scheduler:1.16.9"
|
|
command:
|
|
[
|
|
"./scheduler",
|
|
"--port",
|
|
"50007",
|
|
"--etcd-data-dir=/data",
|
|
"--enable-metrics",
|
|
"--log-level",
|
|
"${DAPR_LOG_LEVEL:-info}",
|
|
]
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
scheduler-init: { condition: service_completed_successfully }
|
|
volumes:
|
|
- scheduler_data:/data
|
|
|
|
############################
|
|
# Monitoring Services (optional profile)
|
|
############################
|
|
|
|
otel-collector:
|
|
profiles: ["monitoring"]
|
|
image: otel/opentelemetry-collector:0.139.0
|
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
|
volumes:
|
|
- ./infra/otel-collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
|
|
depends_on: [jaeger]
|
|
|
|
jaeger:
|
|
profiles: ["monitoring"]
|
|
# image: jaegertracing/jaeger:2.11.0 # v2.x image
|
|
image: jaegertracing/jaeger:2.15.1
|
|
user: "0:0"
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 2G
|
|
reservations:
|
|
memory: 1G
|
|
environment:
|
|
- QUERY_BASE_PATH=/jaeger
|
|
- GOGC=80
|
|
volumes:
|
|
- ./infra/jaeger/jaeger-config.yaml:/etc/jaeger/config.yaml
|
|
- jaeger_data:/badger # Persist trace data
|
|
command: ["--config", "/etc/jaeger/config.yaml"]
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.jaeger.rule=PathPrefix(`/jaeger`)"
|
|
- "traefik.http.services.jaeger.loadbalancer.server.port=16686"
|
|
- "traefik.http.routers.jaeger.entrypoints=websecure"
|
|
- "traefik.http.routers.jaeger.tls=true"
|
|
- "traefik.http.routers.jaeger.middlewares=auth"
|
|
healthcheck:
|
|
test:
|
|
[
|
|
"CMD-SHELL",
|
|
"wget --no-verbose --tries=1 --spider http://localhost:13133/status || exit 1",
|
|
]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
loki:
|
|
profiles: ["monitoring"]
|
|
# image: grafana/loki:3.3.2
|
|
image: grafana/loki:3.5.11
|
|
command:
|
|
["-config.file=/etc/loki/local-config.yaml", "-config.expand-env=true"]
|
|
environment:
|
|
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
|
|
- MINIO_ROOT_USER=${MINIO_ROOT_USER}
|
|
volumes:
|
|
- ./infra/loki:/etc/loki:ro
|
|
- loki_data:/loki
|
|
depends_on: [minio-init]
|
|
healthcheck:
|
|
test:
|
|
[
|
|
"CMD-SHELL",
|
|
"wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1",
|
|
]
|
|
interval: 10s
|
|
|
|
promtail:
|
|
profiles: ["monitoring"]
|
|
# image: grafana/promtail:3.3.2
|
|
image: grafana/promtail:3.5.11
|
|
user: "0:0"
|
|
volumes:
|
|
- ./infra/promtail:/etc/promtail:ro
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
|
command:
|
|
["-config.file=/etc/promtail/config.yaml", "-config.expand-env=true"]
|
|
depends_on: [loki]
|
|
|
|
grafana:
|
|
profiles: ["monitoring"]
|
|
# image: grafana/grafana:11.4.0
|
|
image: grafana/grafana:12.3.4
|
|
environment:
|
|
- GF_AUTH_ANONYMOUS_ENABLED=true
|
|
- GF_AUTH_ANONYMOUS_ORG_ROLE=Editor
|
|
# - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-Qwerty12345}
|
|
# - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-nemesis}
|
|
- GF_SERVER_DOMAIN=${EXTERNAL_HOST:-https://localhost}
|
|
- GF_SERVER_ROOT_URL=${EXTERNAL_HOST:-https://localhost}/grafana
|
|
- GF_SERVER_SERVE_FROM_SUB_PATH=true
|
|
- POSTGRES_CONNECTION_STRING=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-enrichment}?${POSTGRES_PARAMETERS:-sslmode=disable}
|
|
volumes:
|
|
- ./infra/grafana/provisioning:/etc/grafana/provisioning:ro
|
|
- grafana_data:/var/lib/grafana
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.grafana.rule=PathPrefix(`/grafana`)"
|
|
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
|
|
- "traefik.http.routers.grafana.entrypoints=websecure"
|
|
- "traefik.http.routers.grafana.tls=true"
|
|
- "traefik.http.routers.grafana.middlewares=auth"
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-sSf", "http://localhost:3000/"]
|
|
interval: 10s
|
|
|
|
prometheus:
|
|
profiles: ["monitoring"]
|
|
# image: prom/prometheus:v3.1.0
|
|
image: prom/prometheus:v3.10.0
|
|
volumes:
|
|
- ./infra/prometheus:/etc/prometheus:ro
|
|
- prometheus_data:/prometheus
|
|
command:
|
|
[
|
|
"--config.file=/etc/prometheus/prometheus.yml",
|
|
"--storage.tsdb.path=/prometheus",
|
|
"--web.external-url=/prometheus/",
|
|
]
|
|
healthcheck:
|
|
test:
|
|
[
|
|
"CMD",
|
|
"wget",
|
|
"--spider",
|
|
"-q",
|
|
"http://localhost:9090/prometheus/-/healthy",
|
|
]
|
|
interval: 10s
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.prometheus.rule=PathPrefix(`/prometheus`)"
|
|
- "traefik.http.services.prometheus.loadbalancer.server.port=9090"
|
|
- "traefik.http.routers.prometheus.entrypoints=websecure"
|
|
- "traefik.http.routers.prometheus.tls=true"
|
|
- "traefik.http.routers.prometheus.middlewares=auth"
|
|
|
|
node-exporter:
|
|
profiles: ["monitoring"]
|
|
image: prom/node-exporter:v1.10.2
|
|
restart: unless-stopped
|
|
volumes:
|
|
- /proc:/host/proc:ro
|
|
- /sys:/host/sys:ro
|
|
- /:/rootfs:ro
|
|
command:
|
|
[
|
|
"--path.procfs=/host/proc",
|
|
"--path.rootfs=/rootfs",
|
|
"--path.sysfs=/host/sys",
|
|
"--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)",
|
|
]
|
|
|
|
cadvisor:
|
|
profiles: ["monitoring"]
|
|
image: ghcr.io/google/cadvisor:v0.53.0
|
|
restart: unless-stopped
|
|
privileged: true
|
|
command:
|
|
- "--housekeeping_interval=30s"
|
|
- "--storage_duration=2m0s"
|
|
- "--docker_only=true"
|
|
volumes:
|
|
- /:/rootfs:ro
|
|
- /dev/disk/:/dev/disk:ro
|
|
- /sys:/sys:ro
|
|
- /var/lib/docker/:/var/lib/docker:ro
|
|
- /var/run:/var/run:ro
|
|
devices: ["/dev/kmsg:/dev/kmsg"]
|
|
|
|
############################
|
|
# LLM Services (optional profile)
|
|
############################
|
|
|
|
agents:
|
|
profiles: ["llm"]
|
|
image: ghcr.io/specterops/nemesis/agents:latest
|
|
environment:
|
|
- DAPR_HTTP_PORT=3509
|
|
- DAPR_GRPC_PORT=50009
|
|
- NEMESIS_URL=${NEMESIS_URL:?}
|
|
- WORKFLOW_RUNTIME_LOG_LEVEL=${WORKFLOW_RUNTIME_LOG_LEVEL:-WARNING}
|
|
- WORKFLOW_CLIENT_LOG_LEVEL=${WORKFLOW_CLIENT_LOG_LEVEL:-WARNING}
|
|
- LITELLM_ADMIN_KEY=sk-${LITELLM_ADMIN_PASSWORD:-admin123}
|
|
- MAX_BUDGET=${LLM_MAX_BUDGET:-100}
|
|
- BUDGET_DURATION=${LLM_BUDGET_DURATION:-30d}
|
|
- DOTNET_ANALYSIS_RUN_REQUEST_LIMIT=${DOTNET_ANALYSIS_RUN_REQUEST_LIMIT:-25} # max LLM calls to use per .NET program analysis run
|
|
- DOTNET_ANALYSIS_RUN_TOKENS_LIMIT=${DOTNET_ANALYSIS_RUN_TOKENS_LIMIT:-1000000} # max tokens to use per .NET program analysis run
|
|
# if we hit this number of the same triage values for the same file, all future findings get that value
|
|
- TRIAGE_CONSENSUS_THRESHOLD=${TRIAGE_CONSENSUS_THRESHOLD:-3}
|
|
# Phoenix LLM tracing configuration
|
|
- PHOENIX_ENABLED=${PHOENIX_ENABLED:-false}
|
|
- PHOENIX_ENDPOINT=${PHOENIX_ENDPOINT:-http://phoenix:6006/v1/traces}
|
|
- PHOENIX_SQL_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/phoenix
|
|
- NEMESIS_MONITORING=${NEMESIS_MONITORING:-disabled}
|
|
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel-collector:4317
|
|
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_INSECURE=true
|
|
# Chatbot configuration
|
|
- CHATBOT_DB_PASSWORD=${CHATBOT_DB_PASSWORD:-chatbot_pass_change_me}
|
|
- MCP_MAX_RESULTS=${MCP_MAX_RESULTS:-100}
|
|
logging: *logging-config
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
placement: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
hasura: { condition: service_healthy }
|
|
litellm: { condition: service_healthy }
|
|
healthcheck: *healthcheck-python-svc
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.mcp.rule=PathPrefix(`/mcp`)"
|
|
- "traefik.http.services.mcp.loadbalancer.server.port=5000"
|
|
- "traefik.http.routers.mcp.entrypoints=websecure"
|
|
- "traefik.http.routers.mcp.tls=true"
|
|
- "traefik.http.routers.mcp.middlewares=auth"
|
|
agents-dapr:
|
|
profiles: ["llm"]
|
|
image: "daprio/daprd:1.16.9"
|
|
command:
|
|
[
|
|
"./daprd",
|
|
"--max-body-size",
|
|
"1Gi",
|
|
"--app-id",
|
|
"agents",
|
|
"--app-port",
|
|
"8000",
|
|
"--dapr-http-port",
|
|
"3509",
|
|
"--dapr-grpc-port",
|
|
"50009",
|
|
"--placement-host-address",
|
|
"placement:50006",
|
|
"--scheduler-host-address",
|
|
"scheduler:50007",
|
|
"--resources-path",
|
|
"/dapr/components",
|
|
"--config",
|
|
"/dapr/configuration/agents_monitoring_${NEMESIS_MONITORING:-disabled}.yaml",
|
|
"--enable-metrics",
|
|
"--dapr-graceful-shutdown-seconds",
|
|
"5",
|
|
]
|
|
volumes:
|
|
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
|
|
- ./infra/dapr/components/workflowstate.yaml:/dapr/components/workflowstate.yaml:ro
|
|
- ./infra/dapr/configuration/:/dapr/configuration/
|
|
environment:
|
|
- POSTGRES_USER=${POSTGRES_USER:?}
|
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?}
|
|
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
|
|
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
|
- POSTGRES_DB=${POSTGRES_DB:-enrichment}
|
|
- POSTGRES_PARAMETERS=${POSTGRES_PARAMETERS:-sslmode=disable}
|
|
- HASURA_ADMIN_SECRET=${HASURA_ADMIN_SECRET:-pass456}
|
|
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
|
|
- MINIO_ROOT_USER=${MINIO_ROOT_USER:?}
|
|
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:?}
|
|
depends_on:
|
|
agents: { condition: service_started }
|
|
placement: { condition: service_started }
|
|
scheduler: { condition: service_started }
|
|
rabbitmq: { condition: service_healthy }
|
|
network_mode: "service:agents"
|
|
|
|
litellm:
|
|
profiles: ["llm"]
|
|
# image: ghcr.io/berriai/litellm:v1.74.0-stable
|
|
image: ghcr.io/berriai/litellm:v1.81.12-stable.1
|
|
environment:
|
|
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/litellm
|
|
- STORE_MODEL_IN_DB=True
|
|
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
|
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
|
|
- AWS_REGION_NAME=${AWS_REGION_NAME:-us-east-1}
|
|
- LITELLM_MASTER_KEY=sk-${LITELLM_ADMIN_PASSWORD:-admin123}
|
|
- LITELLM_SALT_KEY=sk-${LITELLM_ADMIN_PASSWORD:-admin123}
|
|
- SERVER_ROOT_PATH=/llm
|
|
- PROXY_BASE_URL=${NEMESIS_URL:?}
|
|
- UI_BASE_PATH=/llm/ui
|
|
volumes:
|
|
- ./infra/litellm/config.yml:/app/config.yml:ro
|
|
command: ["--config", "/app/config.yml"]
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
healthcheck:
|
|
test:
|
|
[
|
|
"CMD-SHELL",
|
|
"python3 -c \"import urllib.request,json; r=urllib.request.urlopen(urllib.request.Request('http://localhost:4000/health',headers={'Authorization':'Bearer sk-${LITELLM_ADMIN_PASSWORD:-admin123}'})); d=json.load(r); exit(0 if d.get('healthy_count',0)>0 and d.get('unhealthy_count',0)==0 else 1)\"",
|
|
]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 60s
|
|
logging: *logging-config
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.litellm.rule=PathPrefix(`/llm`)"
|
|
- "traefik.http.services.litellm.loadbalancer.server.port=4000"
|
|
- "traefik.http.routers.litellm.entrypoints=websecure"
|
|
- "traefik.http.routers.litellm.tls=true"
|
|
|
|
phoenix:
|
|
profiles: ["llm"]
|
|
# image: arizephoenix/phoenix:11.24.1
|
|
image: arizephoenix/phoenix:13.3.0
|
|
environment:
|
|
- PHOENIX_WORKING_DIR=/data
|
|
- PHOENIX_HOST_ROOT_PATH=/phoenix
|
|
- PHOENIX_SQL_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/phoenix
|
|
- PHOENIX_DEFAULT_RETENTION_POLICY_DAYS=100
|
|
volumes:
|
|
- phoenix_data:/data
|
|
# ports:
|
|
# - "6006:6006" # Phoenix UI port, for debugging
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.services.phoenix.loadbalancer.server.port=6006"
|
|
- "traefik.http.routers.phoenix.rule=PathPrefix(`/phoenix`)"
|
|
- "traefik.http.routers.phoenix.entrypoints=websecure"
|
|
- "traefik.http.routers.phoenix.tls=true"
|
|
- "traefik.http.routers.phoenix.middlewares=auth,phoenix-stripprefix"
|
|
- "traefik.http.middlewares.phoenix-stripprefix.stripprefix.prefixes=/phoenix"
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
healthcheck:
|
|
test:
|
|
[
|
|
"CMD",
|
|
"python",
|
|
"-c",
|
|
"import urllib.request; urllib.request.urlopen('http://localhost:6006/healthz').read()",
|
|
]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 30s
|