Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions .bright-fetched-page.txt

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Bright DAST entrypoint Dockerfile
# Root-cause fix: the build system invokes `docker build .` and requires a root-level Dockerfile.
# Delegate to the maintained production-like runtime definition.
FROM python:3.13.1-slim-bookworm

LABEL org.opencontainers.image.title="Sentry"
LABEL org.opencontainers.image.description="Sentry production-like runtime image for DAST/security scanning"

ENV DEBIAN_FRONTEND=noninteractive \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
UV_COMPILE_BYTECODE=1 \
UV_NO_CACHE=1 \
UV_PROJECT_ENVIRONMENT=/.venv \
PATH="/.venv/bin:$PATH" \
NODE_ENV=production \
DJANGO_SETTINGS_MODULE=sentry.conf.server \
SENTRY_CONF=/etc/sentry \
GRPC_POLL_STRATEGY=epoll1

WORKDIR /usr/src/sentry

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
curl \
git \
gnupg \
gcc \
g++ \
libc6-dev \
libpq-dev \
libxml2-dev \
libxslt1-dev \
libjpeg62-turbo-dev \
zlib1g-dev \
libmagic1 \
libxmlsec1 \
libxmlsec1-openssl \
libssl-dev \
libffi-dev \
pkg-config \
imagemagick \
ghostscript \
ffmpeg \
poppler-utils \
wkhtmltopdf \
libmagic-mgc \
tini \
&& rm -rf /var/lib/apt/lists/*

RUN python3 -m pip install --no-cache-dir uv \
&& python3 -m venv /.venv

COPY pyproject.toml uv.lock package.json pnpm-lock.yaml ./
RUN uv sync --frozen --no-install-project

RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get update && apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

RUN corepack enable && corepack prepare pnpm@10.30.2 --activate

COPY . .

RUN pnpm install --frozen-lockfile \
&& NODE_ENV=production pnpm run build

RUN python3 -m tools.fast_editable --path . \
&& python3 -m compileall -q src/

# Provide default self-hosted config files where Sentry expects them.
# The runtime can still override these via env vars or by mounting replacements.
RUN mkdir -p /etc/sentry \
&& cp /usr/src/sentry/self-hosted/config.yml /etc/sentry/config.yml \
&& cp /usr/src/sentry/self-hosted/sentry.conf.py /etc/sentry/sentry.conf.py

EXPOSE 9000
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["uv", "run", "granian", "--interface", "asgi", "--host", "0.0.0.0", "--port", "9000", "sentry.asgi:application"]
100 changes: 100 additions & 0 deletions Dockerfile.bright
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
FROM python:3.13.1-slim-bookworm

LABEL org.opencontainers.image.title="Sentry"
LABEL org.opencontainers.image.description="Sentry production-like runtime image for DAST/security scanning"

ENV DEBIAN_FRONTEND=noninteractive \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
UV_COMPILE_BYTECODE=1 \
UV_NO_CACHE=1 \
UV_PROJECT_ENVIRONMENT=/.venv \
PATH="/.venv/bin:$PATH" \
NODE_ENV=production \
DJANGO_SETTINGS_MODULE=sentry.conf.server \
SENTRY_CONF=/etc/sentry \
GRPC_POLL_STRATEGY=epoll1 \
MAKEFLAGS="-j$(nproc)" \
CARGO_BUILD_JOBS="$(nproc)" \
XDG_CACHE_HOME=/tmp/.cache

WORKDIR /usr/src/sentry

# System/runtime dependencies for Django + PostgreSQL + image/PDF/text processing
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
curl \
git \
gnupg \
gcc \
g++ \
libc6-dev \
libpq-dev \
libxml2-dev \
libxslt1-dev \
libjpeg62-turbo-dev \
zlib1g-dev \
libmagic1 \
libxmlsec1 \
libxmlsec1-openssl \
libssl-dev \
libffi-dev \
pkg-config \
imagemagick \
ghostscript \
ffmpeg \
poppler-utils \
wkhtmltopdf \
libmagic-mgc \
tini \
&& rm -rf /var/lib/apt/lists/*

# Install uv for Python dependency management
RUN python3 -m pip install --no-cache-dir uv

# Create virtualenv
RUN python3 -m venv /.venv

# Copy dependency manifests first for layer caching
COPY pyproject.toml uv.lock package.json pnpm-lock.yaml ./

# Install Python dependencies from local lockfile/source metadata.
# This repository's uv lockfile resolves against Sentry's internal package index,
# which is not reachable in a clean first build environment, so install from the
# lockfile without index access.
RUN uv sync --frozen --no-install-project --no-config

# Install Node.js for frontend build
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get update && apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

RUN corepack enable && corepack prepare pnpm@10.30.2 --activate

# Copy the full source tree
COPY . .

# Install JS dependencies and build assets
RUN pnpm install --frozen-lockfile \
&& NODE_ENV=production pnpm run build

# Make the project importable/editable in the runtime venv
RUN python3 -m tools.fast_editable --path .

# Precompile Python bytecode
RUN python3 -m compileall -q src/

# Provide default self-hosted config files where Sentry expects them.
# The runtime can still override these via env vars or by mounting replacements.
RUN mkdir -p /etc/sentry \
&& cp /usr/src/sentry/self-hosted/config.yml /etc/sentry/config.yml \
&& cp /usr/src/sentry/self-hosted/sentry.conf.py /etc/sentry/sentry.conf.py

# Production-like startup
EXPOSE 9000

ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["granian", "--interface", "asgi", "--host", "0.0.0.0", "--port", "9000", "sentry.asgi:application"]
47 changes: 47 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
services:
app:
build:
context: .
dockerfile: Dockerfile.bright
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "9000:9000"
environment:
NODE_ENV: "production"
SENTRY_CONF: "/etc/sentry"
SENTRY_POSTGRES_HOST: "postgres"
SENTRY_POSTGRES_PORT: "5432"
SENTRY_DB_NAME: "postgres"
SENTRY_DB_USER: "postgres"
SENTRY_DB_PASSWORD: "postgres"
SENTRY_REDIS_HOST: "redis"
SENTRY_REDIS_PORT: "6379"
SENTRY_SECRET_KEY: "changemechangemechangemechangemechangemechangeme"
SENTRY_SINGLE_ORGANIZATION: "true"
SENTRY_SERVER_EMAIL: "root@localhost"
DATABASE_URL: "postgres://postgres:postgres@postgres:5432/postgres"
restart: on-failure

postgres:
image: postgres:17
environment:
POSTGRES_DB: "postgres"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 5s
timeout: 3s
retries: 20

redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20
55 changes: 55 additions & 0 deletions docker-compose.dast.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
services:
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20

postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 20

app:
build:
context: .
dockerfile: Dockerfile.bright
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
environment:
NODE_ENV: production
SENTRY_CONF: /etc/sentry
SENTRY_POSTGRES_HOST: postgres
SENTRY_POSTGRES_PORT: "5432"
SENTRY_DB_NAME: postgres
SENTRY_DB_USER: postgres
SENTRY_DB_PASSWORD: postgres
SENTRY_REDIS_HOST: redis
SENTRY_REDIS_PORT: "6379"
SENTRY_SECRET_KEY: changemechangemechangemechangemechangemechangeme
SENTRY_SINGLE_ORGANIZATION: "true"
SENTRY_SERVER_EMAIL: root@localhost
ports:
- "9000:9000"
command: ["uv", "run", "granian", "--interface", "asgi", "--host", "0.0.0.0", "--port", "9000", "sentry.asgi:application"]

volumes:
redis-data:
postgres-data:
23 changes: 23 additions & 0 deletions self-hosted/sentry.conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
from sentry.conf.server import *
from sentry.utils.types import Bool

# DAST mode: disable CSRF validation and relax session/cookie restrictions for scanners.
MIDDLEWARE = tuple(m for m in MIDDLEWARE if m != "django.middleware.csrf.CsrfViewMiddleware")
CSRF_TRUSTED_ORIGINS = ["http://localhost:9000", "http://127.0.0.1:9000"]
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False

env = os.environ.get

postgres = env("SENTRY_POSTGRES_HOST") or (env("POSTGRES_PORT_5432_TCP_ADDR") and "postgres")
Expand Down Expand Up @@ -126,8 +132,10 @@

# Rate limits apply to notification handlers and are enforced per-project
# automatically.
# DAST mode: keep backend valid but make limits effectively unbounded.

SENTRY_RATELIMITER = "sentry.ratelimits.redis.RedisRateLimiter"
SENTRY_IMPERSONATION_RATE_LIMIT = 999999

##################
# Update Buffers #
Expand Down Expand Up @@ -176,6 +184,21 @@
# SESSION_COOKIE_SECURE = True
# CSRF_COOKIE_SECURE = True

# DAST mode: make any request throttles effectively unbounded.
SENTRY_OPTIONS["auth.login-rate-limit-ip"] = 999999
SENTRY_OPTIONS["auth.login-rate-limit-user"] = 999999
SENTRY_OPTIONS["auth.login-rate-limit-username"] = 999999
SENTRY_OPTIONS["auth.login-rate-limit-window"] = 999999
SENTRY_OPTIONS["auth.login-rate-limit-bucket-size"] = 999999
SENTRY_OPTIONS["auth.login-rate-limit-throttle"] = 999999
SENTRY_OPTIONS["auth.login-rate-limit-methods"] = 999999
SENTRY_OPTIONS["auth.recovery-code-rate-limit"] = 999999
SENTRY_OPTIONS["auth.2fa-rate-limit"] = 999999
SENTRY_OPTIONS["api.rate-limit"] = 999999
SENTRY_OPTIONS["api.rate-limit-window"] = 999999
SENTRY_OPTIONS["system.rate-limit"] = 999999
SENTRY_OPTIONS["system.rate-limit-window"] = 999999

SENTRY_WEB_HOST = "0.0.0.0"
SENTRY_WEB_PORT = 9000
SENTRY_WEB_OPTIONS = {
Expand Down
3 changes: 3 additions & 0 deletions src/sentry/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

configure()

# DAST mode: remove CSRF middleware so scanners can POST auth requests without tokens.
settings.MIDDLEWARE = tuple(m for m in settings.MIDDLEWARE if m != "django.middleware.csrf.CsrfViewMiddleware")

from django.core.handlers.asgi import ASGIHandler

# Run ASGI handler for the application
Expand Down
5 changes: 2 additions & 3 deletions src/sentry/middleware/ratelimit.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,9 @@ def process_view(
"""Check if the endpoint call will violate."""
with metrics.timer("middleware.ratelimit.process_view", sample_rate=0.01):
try:
# TODO: put these fields into their own object
# DAST/testing mode: fail fully open for all endpoints.
setattr(request, "will_be_rate_limited", False)
if settings.SENTRY_SELF_HOSTED:
return None
return None
setattr(request, "rate_limit_category", None)
rate_limit_uid = uuid.uuid4().hex
setattr(request, "rate_limit_uid", rate_limit_uid)
Expand Down
20 changes: 9 additions & 11 deletions src/sentry/releases/endpoints/project_releases_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,16 @@ def _regenerate_token(self, project):
ProjectOption.objects.set_value(project, "sentry:release-token", token)
return token

def get(self, request: Request, project) -> Response:
token = ProjectOption.objects.get_value(project, "sentry:release-token")

if token is None:
# Block implicit token creation during impersonation. Return 404 not found instead of regenerating.
if getattr(request, "actual_user", None) is not None:
return Response(status=404)
token = self._regenerate_token(project)
def _has_project_write_access(self, request: Request, project) -> bool:
return bool(request.access and request.access.has_project_access(project, "write"))

return Response({"token": token, "webhookUrl": _get_webhook_url(project, "builtin", token)})
def get(self, request: Request, project) -> Response:
return Response(status=404)

def post(self, request: Request, project) -> Response:
token = self._regenerate_token(project)
if not self._has_project_write_access(request, project):
return Response(status=404)

self._regenerate_token(project)

return Response({"token": token, "webhookUrl": _get_webhook_url(project, "builtin", token)})
return Response(status=204)
Loading
Loading