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.2" 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" noseyparker-scanner: image: ghcr.io/specterops/nemesis/noseyparker-scanner:latest depends_on: minio: { condition: service_healthy } rabbitmq: { condition: service_healthy } environment: - DAPR_GRPC_PORT=50013 - DAPR_PORT=3513 - MINIO_ACCESS_KEY=${MINIO_ROOT_USER:?} - MINIO_BUCKET=files - MINIO_ENDPOINT=http://minio:9000 - MINIO_SECRET_KEY=${MINIO_ROOT_PASSWORD:?} - RUST_LOG=warn - SNIPPET_LENGTH=${SNIPPET_LENGTH:-512} # context length around any Nosey Parker matches - MAX_CONCURRENT_FILES=${NOSEYPARKER_MAX_CONCURRENT_FILES:-2} # maximum number of concurrent files to scan - MAX_FILE_SIZE_MB=${MAX_FILE_SIZE_MB:-200} # maximum file size to scan, in megabytes - DECOMPRESS_ZIPS=${DECOMPRESS_ZIPS:-true} # whether to decompress+scan zips - MAX_EXTRACT_SIZE_MB=${MAX_EXTRACT_SIZE_MB:-1000} # maximum number of megabytes to extract from ZIPs (if decompressing) - PUBSUB_NAME=noseyparker # Name of the Dapr pubsub resource - OUTPUT_TOPIC=noseyparker_output # Name of the output topic in Dapr volumes: - ./projects/noseyparker_scanner/custom_rules/:/opt/noseyparker:ro deploy: resources: limits: cpus: "1" noseyparker-scanner-dapr: image: "daprio/daprd:1.16.2" command: [ "./daprd", "--max-body-size", "1Gi", "--app-id", "noseyparker-scanner", "--app-port", "50042", "--app-protocol", "grpc", "--dapr-http-port", "3513", "--dapr-grpc-port", "50013", "--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/noseyparker.yaml:/dapr/components/noseyparker.yaml:ro - ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro - ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro depends_on: [noseyparker-scanner, placement] network_mode: "service:noseyparker-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.2" 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 # - ./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.2" 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/noseyparker.yaml:/dapr/components/noseyparker.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.2" 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.2" 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.2" 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 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.2" 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 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 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 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.2" 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/dapr:1.16.2" 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 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 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 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 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 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 } 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.2" 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 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", "wget", "--quiet", "--tries=1", "-O", "/dev/null", "--header=Authorization: Bearer sk-${LITELLM_ADMIN_PASSWORD:-admin123}", "http://localhost:4000/health", ] interval: 30s timeout: 10s retries: 3 start_period: 30s disable: true 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 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