-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.dev.yml
More file actions
656 lines (644 loc) · 35.3 KB
/
Copy pathdocker-compose.dev.yml
File metadata and controls
656 lines (644 loc) · 35.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 TRUSCA contributors
# TrustedOSS Portal — development stack.
#
# Compose V1 file format (CLAUDE.md core rule #10: `docker-compose` not
# `docker compose`). Image tags are pinned (rule #9: no `:latest`).
#
# Services:
# postgres — PostgreSQL 17 (primary store)
# redis — Redis 7 (Celery broker + result backend)
# backend — FastAPI w/ uvicorn --reload (hot reload via bind mount)
# celery-worker — Celery worker against redis broker
# frontend — Vite dev server (React 18 + TS, bootstrapped in PR #3)
#
# Bring it up:
# cp .env.example .env
# docker-compose -f docker-compose.dev.yml up -d
# curl http://localhost:8000/health → {"status":"ok"}
x-backend-build: &backend-build
build:
context: ./apps/backend
dockerfile: Dockerfile
# Worker image carries the SCA toolchain (cdxgen / ORT / Trivy + JRE + Node).
# Kept separate from the API image so the API stays slim (~200MB) and the
# worker (~2.5GB) can be rolled / upgraded independently — see CLAUDE.md
# core rule #3 (ORT/cdxgen/Trivy run inside Celery only).
x-worker-build: &worker-build
build:
context: ./apps/backend
dockerfile: Dockerfile.worker
x-backend-env: &backend-env
APP_ENV: ${APP_ENV:-dev}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://trustedoss:trustedoss@postgres:5432/trustedoss}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
SECRET_KEY: ${SECRET_KEY:-change-me-in-dev-only}
ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-30}
REFRESH_TOKEN_EXPIRE_DAYS: ${REFRESH_TOKEN_EXPIRE_DAYS:-7}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:5173}
# Mock vs. real cdxgen / ORT / Trivy. Default `real` so `up` against a fully
# provisioned worker image runs the actual toolchain. CI sets `mock` so the
# e2e job keeps scan duration in seconds (worker emits fixture frames
# instead of forking a 5-60min Java/Node pipeline). See
# apps/backend/integrations/__init__.py for the dispatch contract.
TRUSTEDOSS_SCAN_BACKEND: ${TRUSTEDOSS_SCAN_BACKEND:-real}
# Disable slowapi 5/min login limiter when set (e2e CI only). Empty default
# keeps the limiter enabled in dev / prod. See core/ratelimit.py.
RATELIMIT_DISABLED: ${RATELIMIT_DISABLED:-}
# Marathon bundle 3 (D2) — tasks.backup now drives pg_dump / psql
# directly via DATABASE_URL using postgresql-client-17 baked into the
# backend + worker images. The legacy script-delegation env var
# ``TRUSTEDOSS_SCRIPTS_DIR`` is no longer read by Python; the
# ``./scripts`` host dir stays mounted (below) only for the host-cron
# operator path (``bash scripts/backup.sh`` from outside compose).
# Backup output directory mounted from the host. tasks.backup writes
# here, the admin API lists from the same root.
BACKUPS_ROOT: ${BACKUPS_ROOT:-/opt/trustedoss/backups}
# ---------------------------------------------------------------------------
# Concurrency / stability tuning knobs (work-B / B1 + A1 + A2).
#
# Exposed in dev so an operator can reproduce a prod tuning locally; backend
# reads each via os.getenv() at runtime (CLAUDE.md #11). Defaults match
# .env.example exactly. Dev keeps a single worker at --concurrency=2 (below),
# so these mostly just give you a knob to experiment with.
#
# DB connection pool — async (FastAPI) engine.
DB_POOL_SIZE: ${DB_POOL_SIZE:-5}
DB_MAX_OVERFLOW: ${DB_MAX_OVERFLOW:-3}
DB_POOL_TIMEOUT: ${DB_POOL_TIMEOUT:-30}
DB_POOL_RECYCLE: ${DB_POOL_RECYCLE:-1800}
# DB connection pool — sync (Celery / psycopg2) engine.
DB_SYNC_POOL_SIZE: ${DB_SYNC_POOL_SIZE:-3}
DB_SYNC_MAX_OVERFLOW: ${DB_SYNC_MAX_OVERFLOW:-3}
DB_SYNC_POOL_TIMEOUT: ${DB_SYNC_POOL_TIMEOUT:-30}
DB_SYNC_POOL_RECYCLE: ${DB_SYNC_POOL_RECYCLE:-1800}
# W1: the real worker-count knob (apps/backend/Dockerfile.prod's CMD reads
# it). Dev's own `command:` below runs uvicorn with `--reload`, which does
# not support multiple workers and never reads this var directly. Dev
# always runs exactly 1 uvicorn process regardless of this value. It is
# set here anyway so anything that reads the ACTUAL count (the W2
# boot-time budget warning below) sees the truth rather than the image's
# unrelated default of 4.
UVICORN_WORKERS: ${UVICORN_WORKERS:-1}
# W2: connection-budget fleet-shape hints (see docker-compose.yml for the
# full explanation).
CONN_BUDGET_BACKEND_REPLICAS: ${CONN_BUDGET_BACKEND_REPLICAS:-1}
CONN_BUDGET_WORKER_REPLICAS: ${CONN_BUDGET_WORKER_REPLICAS:-1}
# Scan-trigger abuse controls.
SCAN_TRIGGER_RATE_LIMIT: ${SCAN_TRIGGER_RATE_LIMIT:-20/minute}
SCAN_CONCURRENCY_CAP_PER_TEAM: ${SCAN_CONCURRENCY_CAP_PER_TEAM:-10}
# Scan task time limits + orphan-workspace reclaim grace.
SCAN_SOFT_TIME_LIMIT_SECONDS: ${SCAN_SOFT_TIME_LIMIT_SECONDS:-3600}
SCAN_HARD_TIME_LIMIT_SECONDS: ${SCAN_HARD_TIME_LIMIT_SECONDS:-3900}
WORKSPACE_ORPHAN_MAX_AGE_SECONDS: ${WORKSPACE_ORPHAN_MAX_AGE_SECONDS:-900}
# scancode first-party license detection guards.
SCANCODE_TIMEOUT_SECONDS: ${SCANCODE_TIMEOUT_SECONDS:-600}
SCANCODE_MAX_FILES: ${SCANCODE_MAX_FILES:-20000}
SCANCODE_MAX_DETECTIONS: ${SCANCODE_MAX_DETECTIONS:-5000}
SCANCODE_MAX_RESULT_BYTES: ${SCANCODE_MAX_RESULT_BYTES:-268435456}
# scanoss vendored-OSS identification (BomLens parity Phase J, P3-11). Always
# shipped in the worker image (MIT); gated at runtime by SCANOSS_ENABLED
# (default off). Exposed here so dev can reproduce a prod opt-in locally.
SCANOSS_ENABLED: ${SCANOSS_ENABLED:-false}
SCANOSS_API_URL: ${SCANOSS_API_URL:-https://api.osskb.org}
SCANOSS_API_KEY: ${SCANOSS_API_KEY:-}
SCANOSS_TIMEOUT_SECONDS: ${SCANOSS_TIMEOUT_SECONDS:-300}
# These six were documented in .env.example but never listed here, so
# setting any of them in .env had no effect on the running container:
# compose only passes through a var that is named in this anchor. Found
# while auditing the two opt-in outbound lookups below: an air-gapped site
# following the docs to set EXTERNAL_PACKAGE_LOOKUP_ENABLED=false would not
# actually have turned it off. Defaults match core/config.py and
# .env.example exactly, so an unset .env changes nothing for an existing
# deployment. INTAKE_REQUESTS_ENABLED used to be its own line up near
# RATELIMIT_DISABLED (C8 phase 3), folded in here with the same default and
# the same e2e override in ci.yml.
LICENSE_FETCH_ENABLED: ${LICENSE_FETCH_ENABLED:-true}
SEARCH_RATE_LIMIT: ${SEARCH_RATE_LIMIT:-20/minute}
INTAKE_REQUESTS_ENABLED: ${INTAKE_REQUESTS_ENABLED:-false}
EXTERNAL_PACKAGE_LOOKUP_ENABLED: ${EXTERNAL_PACKAGE_LOOKUP_ENABLED:-true}
EXTERNAL_PACKAGE_LOOKUP_RATE_LIMIT: ${EXTERNAL_PACKAGE_LOOKUP_RATE_LIMIT:-10/minute}
EXTERNAL_ADVISORY_LOOKUP_RATE_LIMIT: ${EXTERNAL_ADVISORY_LOOKUP_RATE_LIMIT:-20/minute}
# Fernet key for secrets-at-rest (GitHub App creds AND — v2.3-s1 — the cosign
# key password). Unset in dev derives from SECRET_KEY (WARNING). See core/crypto.py.
GITHUB_APP_ENCRYPTION_KEY: ${GITHUB_APP_ENCRYPTION_KEY:-}
# A5 - dedicated HMAC key for hashing stored API-key secrets
# (core.config.api_key_hmac_secret). Unset here derives from SECRET_KEY
# (dev/CI convenience, WARNING logged), same resolution as
# GITHUB_APP_ENCRYPTION_KEY above. Wired for symmetry with
# docker-compose.yml even though dev never fails closed on it.
API_KEY_HMAC_SECRET: ${API_KEY_HMAC_SECRET:-}
# cosign SBOM signing (v2.3-s1). Best-effort; key-based default (D2), keyless
# opt-in via COSIGN_KEYLESS=true. Read via os.getenv() at runtime (rule #11).
# The cosign key volume is mounted on celery-worker below from COSIGN_KEYS_HOST_PATH.
COSIGN_KEYLESS: ${COSIGN_KEYLESS:-false}
COSIGN_KEY_PATH: ${COSIGN_KEY_PATH:-}
COSIGN_KEY_PASSWORD_ENCRYPTED: ${COSIGN_KEY_PASSWORD_ENCRYPTED:-}
COSIGN_TIMEOUT_SECONDS: ${COSIGN_TIMEOUT_SECONDS:-120}
# W6-#44 — Trivy DB lifecycle (worker bootstrap + weekly refresh). H-6:
# the cache lives on the shared `trivy-cache` named volume (worker rw,
# backend ro) so the backend's admin/health Trivy panel and admin/disk
# `trivy_db` card read the same metadata.json the worker maintains —
# without the shared mount the backend reported freshness "unknown"
# forever. A named volume is still cleared by `docker-compose down -v`
# like the rest of the dev volumes. All knobs are operator-tunable at
# runtime (rule #11).
TRIVY_CACHE_DIR: ${TRIVY_CACHE_DIR:-/var/lib/trivy}
TRIVY_DB_REPOSITORY: ${TRIVY_DB_REPOSITORY:-ghcr.io/aquasecurity/trivy-db}
TRIVY_DB_REFRESH_HOURS: ${TRIVY_DB_REFRESH_HOURS:-168}
TRIVY_DB_BOOTSTRAP_ON_START: ${TRIVY_DB_BOOTSTRAP_ON_START:-true}
TRIVY_DB_BOOTSTRAP_TIMEOUT_SECONDS: ${TRIVY_DB_BOOTSTRAP_TIMEOUT_SECONDS:-900}
TRIVY_DB_REFRESH_TIMEOUT_SECONDS: ${TRIVY_DB_REFRESH_TIMEOUT_SECONDS:-900}
# compose-env-var wiring audit: OAuth / OIDC social login + GitHub App PR
# comments. Documented in .env.example but never listed in this anchor, so
# setting any of them in .env had no effect on the running container
# (compose only passes through a var named here). Defaults match
# core/config.py exactly, so an unset .env changes nothing for an existing
# deployment.
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
OIDC_ISSUER: ${OIDC_ISSUER:-}
OIDC_SCOPES: ${OIDC_SCOPES:-}
OIDC_GROUPS_CLAIM: ${OIDC_GROUPS_CLAIM:-}
OIDC_GROUP_ROLE_MAP: ${OIDC_GROUP_ROLE_MAP:-}
GITHUB_API_URL: ${GITHUB_API_URL:-https://api.github.com}
GITHUB_APP_TOKEN_HTTP_TIMEOUT_SECONDS: ${GITHUB_APP_TOKEN_HTTP_TIMEOUT_SECONDS:-10}
GITHUB_TOKEN: ${GITHUB_TOKEN:-}
TRUSTEDOSS_GITHUB_TOKEN: ${TRUSTEDOSS_GITHUB_TOKEN:-}
PORTAL_PUBLIC_URL: ${PORTAL_PUBLIC_URL:-}
# compose-env-var wiring audit: vulnerability matching (KEV catalog,
# malicious-package snapshot, reachability analysis), the license/malicious
# build gate, and vulnerability SLA alerting. Documented in .env.example but
# never listed in this anchor, so setting any of them in .env had no effect
# on the running container. Defaults match core/config.py exactly, so an
# unset .env changes nothing for an existing deployment.
GATE_EPSS_THRESHOLD: ${GATE_EPSS_THRESHOLD:-}
GATE_MALICIOUS_ENABLED: ${GATE_MALICIOUS_ENABLED:-true}
GATE_REACHABLE_CRITICAL_ONLY: ${GATE_REACHABLE_CRITICAL_ONLY:-}
KEV_FEED_URL: ${KEV_FEED_URL:-https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json}
KEV_REFRESH_ENABLED: ${KEV_REFRESH_ENABLED:-true}
KEV_REFRESH_TIMEOUT_SECONDS: ${KEV_REFRESH_TIMEOUT_SECONDS:-30}
MALICIOUS_ENABLED: ${MALICIOUS_ENABLED:-true}
MALICIOUS_REFRESH_ENABLED: ${MALICIOUS_REFRESH_ENABLED:-}
MALICIOUS_SNAPSHOT_STALE_DAYS: ${MALICIOUS_SNAPSHOT_STALE_DAYS:-}
MALICIOUS_WAIVE_MAX_DAYS: ${MALICIOUS_WAIVE_MAX_DAYS:-30}
REACHABILITY_ENABLED: ${REACHABILITY_ENABLED:-true}
REACHABILITY_HARD_TIME_LIMIT_SECONDS: ${REACHABILITY_HARD_TIME_LIMIT_SECONDS:-1200}
REACHABILITY_SOFT_TIME_LIMIT_SECONDS: ${REACHABILITY_SOFT_TIME_LIMIT_SECONDS:-900}
VULN_REMATCH_BATCH_SIZE: ${VULN_REMATCH_BATCH_SIZE:-50}
VULN_REMATCH_INTERVAL_HOURS: ${VULN_REMATCH_INTERVAL_HOURS:-6}
VULN_REMATCH_LOCK_SKEW_SECONDS: ${VULN_REMATCH_LOCK_SKEW_SECONDS:-30}
VULN_SLA_ALERTS_ENABLED: ${VULN_SLA_ALERTS_ENABLED:-true}
VULN_SLA_DAYS_CRITICAL: ${VULN_SLA_DAYS_CRITICAL:-7}
VULN_SLA_DAYS_HIGH: ${VULN_SLA_DAYS_HIGH:-30}
VULN_SLA_DAYS_MEDIUM: ${VULN_SLA_DAYS_MEDIUM:-90}
VULN_SLA_DAYS_LOW: ${VULN_SLA_DAYS_LOW:-180}
# compose-env-var wiring audit: scan executor selection, Docker sidecar
# hardening (resource caps, capabilities, network, volume strategy), Android
# scan image selection, and scan-log persistence/retention. Documented in
# .env.example but never listed in this anchor, so setting any of them in
# .env had no effect on the running container. Defaults match
# core/config.py exactly, so an unset .env changes nothing for an existing
# deployment.
SCAN_ALLOW_UNPINNED_IMAGE: ${SCAN_ALLOW_UNPINNED_IMAGE:-}
SCAN_ANDROID_API_DEFAULT: ${SCAN_ANDROID_API_DEFAULT:-34}
SCAN_ANDROID_IMAGE_PREFIX: ${SCAN_ANDROID_IMAGE_PREFIX:-ghcr.io/sktelecom/sbom-scanner-android-sdk}
SCAN_ANDROID_IMAGE_TAG: ${SCAN_ANDROID_IMAGE_TAG:-latest}
SCAN_AVERAGE_DURATION_SECONDS: ${SCAN_AVERAGE_DURATION_SECONDS:-1200}
SCAN_DOCKER_VOLUME_STRATEGY: ${SCAN_DOCKER_VOLUME_STRATEGY:-named}
SCAN_EXECUTOR: ${SCAN_EXECUTOR:-inprocess}
SCAN_LOCAL_DOCKER_ENVS: ${SCAN_LOCAL_DOCKER_ENVS:-android}
SCAN_LOG_LINE_MAX_LEN: ${SCAN_LOG_LINE_MAX_LEN:-2000}
SCAN_LOG_MAX_LINES_PER_SCAN: ${SCAN_LOG_MAX_LINES_PER_SCAN:-20000}
SCAN_LOG_PERSIST_ENABLED: ${SCAN_LOG_PERSIST_ENABLED:-true}
SCAN_QUEUE_SLOT_COUNT: ${SCAN_QUEUE_SLOT_COUNT:-2}
SCAN_RETENTION_KEEP_LAST: ${SCAN_RETENTION_KEEP_LAST:-30}
SCAN_RETENTION_MAX_AGE_DAYS: ${SCAN_RETENTION_MAX_AGE_DAYS:-180}
SCAN_RETENTION_SUPERSEDED_GRACE_DAYS: ${SCAN_RETENTION_SUPERSEDED_GRACE_DAYS:-7}
SCAN_SIDECAR_CAP_ADD: ${SCAN_SIDECAR_CAP_ADD:-CHOWN,DAC_OVERRIDE,FOWNER,SETGID,SETUID}
SCAN_SIDECAR_CAP_DROP: ${SCAN_SIDECAR_CAP_DROP:-ALL}
SCAN_SIDECAR_CPUS: ${SCAN_SIDECAR_CPUS:-2}
SCAN_SIDECAR_MEMORY: ${SCAN_SIDECAR_MEMORY:-4g}
SCAN_SIDECAR_NETWORK: ${SCAN_SIDECAR_NETWORK:-}
SCAN_SIDECAR_PIDS_LIMIT: ${SCAN_SIDECAR_PIDS_LIMIT:-4096}
SCAN_VOLUMES_FROM_ACK: ${SCAN_VOLUMES_FROM_ACK:-}
SCAN_WORKER_CONTAINER: ${SCAN_WORKER_CONTAINER:-}
SCAN_WORKSPACE_MOUNT: ${SCAN_WORKSPACE_MOUNT:-/tmp/trustedoss}
SCAN_WORKSPACE_VOLUME: ${SCAN_WORKSPACE_VOLUME:-}
STALE_RUNNING_SCAN_GRACE_SECONDS: ${STALE_RUNNING_SCAN_GRACE_SECONDS:-900}
# compose-env-var wiring audit: source/SBOM ingest and download size
# limits (raw upload tarballs, source archives, SBOM upload/download,
# dependency-graph node cap, VEX import, npm manifest parsing) and SBOM
# conformance thresholds. Documented in .env.example but never listed in
# this anchor, so setting any of them in .env had no effect on the running
# container. Defaults match core/config.py exactly, so an unset .env
# changes nothing for an existing deployment.
DEPENDENCY_GRAPH_MAX_NODES: ${DEPENDENCY_GRAPH_MAX_NODES:-5000}
NPM_MANIFEST_MAX_BYTES: ${NPM_MANIFEST_MAX_BYTES:-1048576}
SBOM_CONFORMANCE_FIELD_MIN_PCT: ${SBOM_CONFORMANCE_FIELD_MIN_PCT:-80}
SBOM_CONFORMANCE_HASH_MIN_PCT: ${SBOM_CONFORMANCE_HASH_MIN_PCT:-50}
SBOM_CONFORMANCE_LICENSE_MIN_PCT: ${SBOM_CONFORMANCE_LICENSE_MIN_PCT:-80}
SBOM_CONFORMANCE_PURL_MIN_PCT: ${SBOM_CONFORMANCE_PURL_MIN_PCT:-90}
SBOM_DOWNLOAD_MAX_BYTES: ${SBOM_DOWNLOAD_MAX_BYTES:-67108864}
SBOM_INGEST_MAX_BYTES: ${SBOM_INGEST_MAX_BYTES:-33554432}
SBOM_INGEST_MAX_COMPONENTS: ${SBOM_INGEST_MAX_COMPONENTS:-50000}
SCAN_SOURCE_MAX_TARBALL_BYTES: ${SCAN_SOURCE_MAX_TARBALL_BYTES:-536870912}
SCAN_SOURCE_PROJECT_QUOTA_BYTES: ${SCAN_SOURCE_PROJECT_QUOTA_BYTES:-1073741824}
SCAN_SOURCE_RAW_DOWNLOAD_MAX_BYTES: ${SCAN_SOURCE_RAW_DOWNLOAD_MAX_BYTES:-536870912}
SCAN_SOURCE_RETENTION: ${SCAN_SOURCE_RETENTION:-latest}
SCAN_SOURCE_VIEWER_MAX_FILE_BYTES: ${SCAN_SOURCE_VIEWER_MAX_FILE_BYTES:-2097152}
SOURCE_ARCHIVE_MAX_BYTES: ${SOURCE_ARCHIVE_MAX_BYTES:-104857600}
SOURCE_ARCHIVE_MAX_COMPRESSION_RATIO: ${SOURCE_ARCHIVE_MAX_COMPRESSION_RATIO:-200}
SOURCE_ARCHIVE_MAX_EXTRACTED_BYTES: ${SOURCE_ARCHIVE_MAX_EXTRACTED_BYTES:-1073741824}
SOURCE_ARCHIVE_MAX_MEMBERS: ${SOURCE_ARCHIVE_MAX_MEMBERS:-50000}
SOURCE_ARCHIVE_PROJECT_QUOTA_BYTES: ${SOURCE_ARCHIVE_PROJECT_QUOTA_BYTES:-524288000}
SOURCE_ARCHIVE_RATIO_GUARD_MIN_BYTES: ${SOURCE_ARCHIVE_RATIO_GUARD_MIN_BYTES:-10485760}
SOURCE_ARCHIVE_RETENTION_HOURS: ${SOURCE_ARCHIVE_RETENTION_HOURS:-24}
VEX_IMPORT_MAX_BYTES: ${VEX_IMPORT_MAX_BYTES:-8388608}
VEX_IMPORT_MAX_STATEMENTS: ${VEX_IMPORT_MAX_STATEMENTS:-100000}
# compose-env-var wiring audit: cdxgen image/spec selection, govulncheck
# subprocess limits, scancode enable flag, and the shared per-language
# toolchain cache (size/idle/roots). Documented in .env.example but never
# listed in this anchor, so setting any of them in .env had no effect on
# the running container. Defaults match core/config.py exactly, so an
# unset .env changes nothing for an existing deployment.
#
# NOT included here: EXTRACTCODE_LIBARCHIVE_PATH / TYPECODE_LIBMAGIC_PATH.
# Both are also documented in .env.example and also missing from this
# anchor, but Dockerfile.worker bakes them as image ENV (pointing
# scancode's ctypes loader at the arm64 system libarchive/libmagic, see
# integrations/_subprocess_env.py). Adding an `${VAR:-}` line here would
# shadow that image default with an empty string and crash scancode on
# import, which is worse than the gap it would "fix". They stay
# unreferenced deliberately.
CDXGEN_ALLINONE_IMAGE: ${CDXGEN_ALLINONE_IMAGE:-ghcr.io/cyclonedx/cdxgen:v12.5.0}
CDXGEN_FETCH_LICENSE: ${CDXGEN_FETCH_LICENSE:-}
CDXGEN_IMAGE_TAG: ${CDXGEN_IMAGE_TAG:-v12}
CDXGEN_SPEC_VERSION: ${CDXGEN_SPEC_VERSION:-1.5}
GOVULNCHECK_MAX_OUTPUT_BYTES: ${GOVULNCHECK_MAX_OUTPUT_BYTES:-67108864}
GOVULNCHECK_TIMEOUT_SECONDS: ${GOVULNCHECK_TIMEOUT_SECONDS:-600}
SCANCODE_ENABLED: ${SCANCODE_ENABLED:-true}
TOOLCHAIN_CACHE_IDLE_SECONDS: ${TOOLCHAIN_CACHE_IDLE_SECONDS:-900}
TOOLCHAIN_CACHE_MAX_BYTES: ${TOOLCHAIN_CACHE_MAX_BYTES:-8589934592}
TOOLCHAIN_CACHE_ROOTS: ${TOOLCHAIN_CACHE_ROOTS:-}
# compose-env-var wiring audit: self-registration policy, default team
# role, API-key bookkeeping, and permission/token retention. Documented in
# .env.example but never listed in this anchor, so setting any of them in
# .env had no effect on the running container. Defaults match
# core/config.py exactly, so an unset .env changes nothing for an existing
# deployment.
API_KEY_LAST_USED_AT_UPDATE_INTERVAL_SECONDS: ${API_KEY_LAST_USED_AT_UPDATE_INTERVAL_SECONDS:-900}
API_KEY_VERIFICATION_MIN_DURATION_SECONDS: ${API_KEY_VERIFICATION_MIN_DURATION_SECONDS:-0.22}
AUTH_AUTO_REGISTER: ${AUTH_AUTO_REGISTER:-false}
AUTH_REGISTER_CREATES_TEAM: ${AUTH_REGISTER_CREATES_TEAM:-true}
AUTH_SELF_REGISTRATION: ${AUTH_SELF_REGISTRATION:-true}
DEFAULT_MEMBER_ROLE: ${DEFAULT_MEMBER_ROLE:-}
PERMISSION_CACHE_TTL_SECONDS: ${PERMISSION_CACHE_TTL_SECONDS:-}
PASSWORD_RESET_TOKEN_RETENTION_GRACE_DAYS: ${PASSWORD_RESET_TOKEN_RETENTION_GRACE_DAYS:-1}
REFRESH_TOKEN_RETENTION_GRACE_DAYS: ${REFRESH_TOKEN_RETENTION_GRACE_DAYS:-1}
# compose-env-var wiring audit: the audit-log export webhook (batch size,
# lag, auth token, target URL), the audit hash-chain key, and various
# retention windows (audit log, notifications, report downloads, webhook
# deliveries, Celery task-run records) plus the pg_dump/psql subprocess
# timeout. Documented in .env.example but never listed in this anchor, so
# setting any of them in .env had no effect on the running container.
# Defaults match core/config.py exactly, so an unset .env changes nothing
# for an existing deployment.
AUDIT_EXPORT_BATCH_SIZE: ${AUDIT_EXPORT_BATCH_SIZE:-500}
AUDIT_EXPORT_LAG_SECONDS: ${AUDIT_EXPORT_LAG_SECONDS:-30}
AUDIT_EXPORT_TOKEN: ${AUDIT_EXPORT_TOKEN:-}
AUDIT_EXPORT_URL: ${AUDIT_EXPORT_URL:-}
AUDIT_HASH_KEY: ${AUDIT_HASH_KEY:-}
AUDIT_LOG_RETENTION_DAYS: ${AUDIT_LOG_RETENTION_DAYS:-90}
BACKUP_SUBPROCESS_TIMEOUT: ${BACKUP_SUBPROCESS_TIMEOUT:-3600}
NOTIFICATION_RETENTION_DAYS: ${NOTIFICATION_RETENTION_DAYS:-180}
REPORT_DOWNLOAD_RETENTION_DAYS: ${REPORT_DOWNLOAD_RETENTION_DAYS:-365}
TASK_RUN_RETENTION_DAYS: ${TASK_RUN_RETENTION_DAYS:-90}
WEBHOOK_DELIVERY_RETENTION_DAYS: ${WEBHOOK_DELIVERY_RETENTION_DAYS:-90}
# compose-env-var wiring audit: admin/health thresholds (disk, active-scan
# count), Prometheus metrics exporter, scan-queue backlog alerting, webhook
# capacity retry, and WebSocket connection caps. Documented in .env.example
# but never listed in this anchor, so setting any of them in .env had no
# effect on the running container. Defaults match core/config.py exactly,
# so an unset .env changes nothing for an existing deployment.
DISK_THRESHOLD_CRITICAL_PCT: ${DISK_THRESHOLD_CRITICAL_PCT:-90.0}
DISK_THRESHOLD_WARNING_PCT: ${DISK_THRESHOLD_WARNING_PCT:-80.0}
HEALTH_ACTIVE_SCANS_CRIT: ${HEALTH_ACTIVE_SCANS_CRIT:-200}
HEALTH_ACTIVE_SCANS_WARN: ${HEALTH_ACTIVE_SCANS_WARN:-50}
METRICS_ENABLED: ${METRICS_ENABLED:-false}
METRICS_TOKEN: ${METRICS_TOKEN:-}
QUEUE_BACKLOG_ALERT_COOLDOWN_SECONDS: ${QUEUE_BACKLOG_ALERT_COOLDOWN_SECONDS:-3600}
QUEUE_BACKLOG_ALERT_DEFAULT_QUEUE_THRESHOLD: ${QUEUE_BACKLOG_ALERT_DEFAULT_QUEUE_THRESHOLD:-100}
QUEUE_BACKLOG_ALERT_ENABLED: ${QUEUE_BACKLOG_ALERT_ENABLED:-false}
QUEUE_BACKLOG_ALERT_SCAN_QUEUE_THRESHOLD: ${QUEUE_BACKLOG_ALERT_SCAN_QUEUE_THRESHOLD:-10}
QUEUE_BACKLOG_ALERT_SUSTAIN_SECONDS: ${QUEUE_BACKLOG_ALERT_SUSTAIN_SECONDS:-600}
QUEUE_BACKLOG_METRICS_ENABLED: ${QUEUE_BACKLOG_METRICS_ENABLED:-false}
WEBHOOK_CAPACITY_RETRY_ENABLED: ${WEBHOOK_CAPACITY_RETRY_ENABLED:-true}
WEBSOCKET_MAX_CONNECTIONS_GLOBAL: ${WEBSOCKET_MAX_CONNECTIONS_GLOBAL:-500}
WEBSOCKET_MAX_CONNECTIONS_PER_USER: ${WEBSOCKET_MAX_CONNECTIONS_PER_USER:-8}
# compose-env-var wiring audit: license-metadata enrichment sources
# (ClearlyDefined, OSORI) and the license/malicious-package waiver policy.
# Documented in .env.example but never listed in this anchor, so setting
# any of them in .env had no effect on the running container. Defaults
# match core/config.py exactly, so an unset .env changes nothing for an
# existing deployment.
CLEARLYDEFINED_ENABLED: ${CLEARLYDEFINED_ENABLED:-}
LICENSE_FETCH_TTL_SECONDS: ${LICENSE_FETCH_TTL_SECONDS:-86400}
LICENSE_WAIVE_MAX_DAYS: ${LICENSE_WAIVE_MAX_DAYS:-90}
OSORI_ENABLED: ${OSORI_ENABLED:-true}
OSORI_SNAPSHOT_PATH: ${OSORI_SNAPSHOT_PATH:-}
# compose-env-var wiring audit: demo SaaS mode (sandbox-scan allowance,
# read-only lockdown) and the seed_demo.py bootstrap super-admin password
# override. Documented in .env.example but never listed in this anchor, so
# setting any of them in .env had no effect on the running container.
# Defaults match core/config.py exactly, so an unset .env changes nothing
# for an existing deployment.
DEMO_ALLOW_SANDBOX_SCANS: ${DEMO_ALLOW_SANDBOX_SCANS:-false}
DEMO_READ_ONLY: ${DEMO_READ_ONLY:-false}
DEMO_SUPER_ADMIN_PASSWORD: ${DEMO_SUPER_ADMIN_PASSWORD:-}
# compose-env-var wiring audit: three small, unrelated clusters bundled
# into one PR since each is only 2-3 vars: cosign public-key path override
# + SLSA provenance builder ID, the generic ticket webhook (events/token/
# URL), and two API rate limits. Documented in .env.example but never
# listed in this anchor, so setting any of them in .env had no effect on
# the running container. Defaults match core/config.py exactly, so an
# unset .env changes nothing for an existing deployment.
COSIGN_PUBLIC_KEY_PATH: ${COSIGN_PUBLIC_KEY_PATH:-}
SLSA_BUILDER_ID: ${SLSA_BUILDER_ID:-https://github.com/trustedoss/trusca/worker}
TICKET_WEBHOOK_EVENTS: ${TICKET_WEBHOOK_EVENTS:-}
TICKET_WEBHOOK_TOKEN: ${TICKET_WEBHOOK_TOKEN:-}
TICKET_WEBHOOK_URL: ${TICKET_WEBHOOK_URL:-}
API_READ_RATE_LIMIT: ${API_READ_RATE_LIMIT:-60/minute}
CSV_EXPORT_RATE_LIMIT: ${CSV_EXPORT_RATE_LIMIT:-5/minute}
services:
postgres:
image: postgres:17.2-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-trustedoss}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-trustedoss}
POSTGRES_DB: ${POSTGRES_DB:-trustedoss}
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-trustedoss} -d ${POSTGRES_DB:-trustedoss}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
redis:
image: redis:7.4-alpine
restart: unless-stopped
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
start_period: 5s
backend:
<<: *backend-build
image: trustedoss/backend:dev
restart: unless-stopped
# `--reload-exclude` keeps watchfiles from triggering a reload when only
# tests / venv / __pycache__ change — repeated user-test sessions edited
# `tests/unit/*.py` and the reload loop hung graceful-shutdown until the
# backend container went unhealthy.
# `--timeout-graceful-shutdown 5` caps the shutdown wait so the next
# boot doesn't sit on "Waiting for background tasks to complete." for
# minutes when a slow Celery thread is still alive.
command: >-
uvicorn main:app --host 0.0.0.0 --port 8000
--reload
--reload-exclude 'tests/*'
--reload-exclude '**/__pycache__/*'
--reload-exclude '.mypy_cache/*'
--reload-exclude '.ruff_cache/*'
--timeout-graceful-shutdown 5
environment:
<<: *backend-env
volumes:
- ./apps/backend:/app
# Marathon bundle 3 (D2) — backups land under
# /opt/trustedoss/backups (BACKUPS_ROOT). The ./scripts mount stays
# for the host operator path only; tasks.backup no longer reads it.
#
# A named volume, not ./backups. The worker runs as uid 1000 (#65) and a
# host bind mount carries the host's ownership: docker creates ./backups
# root-owned when it is missing, and a pre-created one belongs to whoever
# ran mkdir. Neither is uid 1000, so every API-triggered backup died with
# EACCES on the timestamped subdirectory. The volume inherits the image's
# ownership instead, which both images pin to 1000.
# `scripts/backup.sh` is the operator's HOST path and still writes to
# ./backups on the host — the two are separate stores by design.
- ./scripts:/opt/trustedoss/scripts:ro
- backups-data:/opt/trustedoss/backups
# Shared scan workspace so backend-saved uploaded archives
# (zip-upload source input) are visible to the worker that extracts
# and scans them. WORKSPACE_HOST_PATH defaults to /tmp/trustedoss.
- scan-workspace:/tmp/trustedoss
# H-6 — read-only view of the worker-maintained Trivy DB cache so
# /v1/admin/health (Trivy DB panel) and /v1/admin/disk (trivy_db card)
# report the real DB state instead of "unknown".
- trivy-cache:/var/lib/trivy:ro
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
# v2.1 Track B (B1) — READINESS gate (schema == Alembic HEAD), so the
# worker / beat `service_healthy` gates below mean "schema migrated", not
# just "uvicorn answers". With AUTO_MIGRATE=true (the dev default) backend
# runs `alembic upgrade head` on start and /health/ready flips to 200 once
# it finishes. This works because the dev image (apps/backend/Dockerfile)
# routes CMD through docker-entrypoint.sh via ENTRYPOINT — the entrypoint
# applies the migration before exec'ing uvicorn (same path as the prod
# image). The pure-liveness /health route still exists for liveness-only
# consumers. (See docs-site deployment notes.)
test: ["CMD-SHELL", "curl -fsS http://localhost:8000/health/ready || exit 1"]
interval: 10s
timeout: 5s
retries: 6
start_period: 60s
celery-worker:
<<: *worker-build
image: trustedoss/backend-worker:dev
restart: unless-stopped
# `-Q` explicit since S3 (concurrency-scaling-plan-2026-08-22.md) split
# scan-pipeline tasks onto a dedicated `trustedoss.scan` queue via
# `task_routes` (tasks/celery_app.py) - a worker started with no `-Q`
# only binds the implicit `task_default_queue` (`trustedoss.default`),
# so this single dev worker stopped ever consuming a triggered scan the
# moment that routing landed. Confirmed locally with
# `celery inspect active_queues` before this fix (only trustedoss.default
# was bound) and after (both are). Matches the queue list production's
# worker-scan / worker-default both consume by default in
# docker-compose.yml. Root cause of the open weekly-dogfood-scan alert
# (#210) - dogfood-scan.yml triggers a real scan against this same
# service and its poll never sees the task leave "queued".
command: celery -A tasks.celery_app worker --loglevel=info --concurrency=2 -Q trustedoss.default,trustedoss.scan
environment:
<<: *backend-env
# bug-hunter 검증용: 가장 느린 scancode(첫 파티 소스 라이선스) 단계를 스킵해 real 스캔을 단축한다.
# 컴포넌트(cdxgen)·취약점(Trivy)은 그대로 — 핵심 데이터 유지. 검증 후 이 줄을 지우면 원복된다.
SCANCODE_MAX_FILES: "0"
volumes:
- ./apps/backend:/app
# Marathon bundle 3 (D2) — tasks.backup writes via pg_dump directly.
# Named volume — see the backend service for why it is not ./backups.
- ./scripts:/opt/trustedoss/scripts:ro
- backups-data:/opt/trustedoss/backups
# Shared scan workspace (see backend) — the worker reads uploaded
# archives + extracts/clones source here.
- scan-workspace:/tmp/trustedoss
# v2.3-s1 — cosign signing keys (read-only). Default ./secrets/cosign
# (created by scripts/cosign-keygen.sh). Mounted at /cosign so
# COSIGN_KEY_PATH=/cosign/cosign.key. Empty/absent → unsigned (best-effort).
- ${COSIGN_KEYS_HOST_PATH:-./secrets/cosign}:/cosign:ro
# W6-#44 / H-6 — Trivy DB cache (bootstrap + weekly refresh write here).
# Shared with backend (read-only there) for health / disk telemetry.
- trivy-cache:/var/lib/trivy
# Dynamic-scan executor (SCAN_EXECUTOR=local_docker) — the worker launches
# a per-environment cdxgen sidecar (e.g. the Android SDK image) via the host
# Docker socket. OPT-IN: only used when SCAN_EXECUTOR=local_docker; the
# default inprocess executor never touches it. On-prem only — the socket is
# a host-escape surface (increment 6 hardens it behind a socket proxy).
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# v2.1 Track B (B1) — wait for the SCHEMA to be at HEAD, not just for
# Postgres to accept connections. backend's healthcheck probes
# /health/ready (schema == Alembic HEAD), and with AUTO_MIGRATE=true (dev
# default) backend applies the migration on start, so this gate ensures the
# dev worker never runs a task against a half-migrated schema after a fresh
# `docker-compose up` that includes a new revision.
backend:
condition: service_healthy
healthcheck:
test:
- "CMD-SHELL"
- "celery -A tasks.celery_app inspect ping --timeout=5 -d celery@$$HOSTNAME > /dev/null 2>&1 || exit 1"
interval: 30s
timeout: 15s
retries: 5
start_period: 60s
# Celery Beat — picks up the periodic schedule registered in
# tasks.celery_app (`dt-health-heartbeat`, `dt-resync-hourly`,
# `dt-orphan-cleaner-six-hourly`). Without this sidecar the worker
# only runs ad-hoc tasks; the dt_resync hourly cadence (and the
# dt_health 60s heartbeat) were registered in PR #8 but had no
# process actually firing them.
#
# Re-uses the worker image so we only build / cache one heavy image.
celery-beat:
<<: *worker-build
image: trustedoss/backend-worker:dev
restart: unless-stopped
command: celery -A tasks.celery_app beat --loglevel=info --schedule=/tmp/celerybeat-schedule
environment:
<<: *backend-env
volumes:
- ./apps/backend:/app
# Marathon bundle 3 (D2) — beat imports tasks.backup at startup;
# the task drives pg_dump / psql directly, no script delegation.
# Named volume — see the backend service for why it is not ./backups.
- ./scripts:/opt/trustedoss/scripts:ro
- backups-data:/opt/trustedoss/backups
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
celery-worker:
condition: service_healthy
frontend:
build:
context: ./apps/frontend
dockerfile: Dockerfile
image: trustedoss/frontend:dev
restart: unless-stopped
environment:
VITE_API_BASE_URL: ${VITE_API_BASE_URL:-http://localhost:8000}
volumes:
# Bind-mount the source for HMR. The anonymous volume on
# /app/node_modules masks the host directory so the image's deps win
# over whatever the host has (or doesn't) under apps/frontend/.
- ./apps/frontend:/app
- /app/node_modules
ports:
- "5173:5173"
healthcheck:
# Use 127.0.0.1 (IPv4): node:alpine resolves `localhost` to IPv6 (::1)
# first while Vite binds to 0.0.0.0 only, which would fail the probe.
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5173/ > /dev/null 2>&1 || exit 1"]
interval: 10s
timeout: 5s
retries: 6
start_period: 60s
# ---- Docker socket proxy — opt-in hardening for SCAN_EXECUTOR=local_docker ---
# The dynamic-scan sidecar needs the Docker API, but mounting the raw socket on
# the worker grants root-equivalent host control. This Tecnativa proxy mounts
# the socket read-only and exposes a filtered TCP API that allows ONLY the
# endpoints the executor uses (containers + images; POST to create/start/pull)
# and blocks exec, swarm, networks, volumes, and the rest. It shrinks the API
# surface; it does NOT inspect create payloads, so the executor's own
# --cap-drop / no-new-privileges on the containers it launches stay essential.
#
# Opt-in: started only with `docker-compose --profile local-docker up`. To use
# the hardened path, set the worker's DOCKER_HOST=tcp://docker-socket-proxy:2375
# and REMOVE the raw /var/run/docker.sock mount from celery-worker above.
docker-socket-proxy:
image: ghcr.io/tecnativa/docker-socket-proxy:0.3.0
profiles: ["local-docker"]
restart: unless-stopped
environment:
CONTAINERS: "1" # GET /containers/* (list / inspect / logs)
IMAGES: "1" # GET /images/* + image pull
POST: "1" # allow POST (create / start / pull) on the allowed resources
DELETE: "1" # allow DELETE /containers/<id> so `docker rm -f` cleanup works
EXEC: "0" # NO `docker exec`
VOLUMES: "0"
NETWORKS: "0"
SWARM: "0"
SYSTEM: "0"
INFO: "0"
AUTH: "0"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
volumes:
postgres-data:
# Shared between backend (uploaded-archive save) and worker (extract/scan)
# so zip-upload source scans can hand the archive across containers.
scan-workspace:
# H-6 — Trivy vulnerability DB cache, written by the worker (bootstrap +
# weekly refresh) and read by the backend (admin health / disk panels).
trivy-cache:
# API-triggered backups (manual from the admin UI, automatic from the beat
# job). Shared by backend (list / download / restore), worker (writes) and
# beat. Owned by uid 1000, which is what the worker runs as — see the
# backend service's mount for the bind-mount ownership problem this avoids.
backups-data:
networks:
# Isolated egress network for dynamic-scan sidecars (SCAN_EXECUTOR=local_docker).
# The untrusted build needs package registries (gradle → google/maven) so it
# keeps internet egress, but it is a SEPARATE network from the default one the
# app services share — so a malicious build cannot reach postgres / redis / the
# backend (SSRF / internal pivot). Point the sidecar at it with
# SCAN_SIDECAR_NETWORK=<compose-project>_scan-egress. A production deployment
# should further constrain egress to the needed registries via a firewall/proxy.
scan-egress:
driver: bridge