-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path.env.example
More file actions
811 lines (768 loc) · 45.1 KB
/
Copy path.env.example
File metadata and controls
811 lines (768 loc) · 45.1 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
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
# TrustedOSS Portal — example environment file.
# Copy to `.env` and fill in real values:
# cp .env.example .env
# ---------- Application ----------
APP_ENV=dev
LOG_LEVEL=INFO
# Comma-separated list. Production must enumerate origins explicitly.
CORS_ALLOWED_ORIGINS=http://localhost:5173
# Apply Alembic migrations automatically when the backend container starts
# (default true). Set false if you manage the schema out-of-band — e.g. a
# separate migration Job, or an L1 role-separated stack where install.sh /
# upgrade.sh run alembic as the owner role. Forward-only: only ever runs
# `alembic upgrade head`, never downgrades.
AUTO_MIGRATE=true
# ---------- Edge / TLS (production: docker-compose.yml + Traefik) ----------
# DOMAIN powers Traefik's Host(`...`) rule. For localhost dev this stays
# empty (the dev compose overlay does not run Traefik).
DOMAIN=
# Contact address Let's Encrypt uses for expiry notices and rate-limit
# escalation. Required when DOMAIN is set and the public URL is https://;
# scripts/install.sh prompts for it. Leave empty for HTTP-only / local.
TLS_EMAIL=
# Traefik log level: DEBUG | INFO | WARN | ERROR. INFO is the safe default.
TRAEFIK_LOG_LEVEL=INFO
# IMAGE_REGISTRY is the namespace the production compose stack pulls images
# from: <registry>/trusca-backend, <registry>/trusca-backend-worker,
# <registry>/trusca-frontend. Default is GitHub Container Registry under the
# trustedoss org, published by .github/workflows/release.yml on each vX.Y.Z
# tag. Override only for a fork, a private mirror, or an air-gapped registry.
# (Releases <= 0.10.0 were published under the pre-rename names backend /
# backend-worker / frontend; those tags stay where they are.)
IMAGE_REGISTRY=ghcr.io/trustedoss
# IMAGE_TAG pins the container tags pulled by the production compose stack.
# Bump this on every release; never use :latest. The release workflow
# publishes both an immutable X.Y.Z tag and a movable X.Y tag. The trusca-*
# image names start at 0.11.0; 0.11.1 is a UI / branding patch.
IMAGE_TAG=0.18.0
# ---------- Evaluation (lightweight 2 vCPU / 4 GB profile) ----------
# Used by the eval overlay + scripts/eval-up.sh:
# docker-compose -f docker-compose.yml -f docker-compose.eval.yml up -d
# The eval stack runs WITHOUT Dependency-Track (no 4 GB JVM heap) and serves
# the seeded / cached vulnerability data via the DT circuit breaker (OPEN). It
# is sized for BROWSING a demo dataset, not for production scanning.
#
# EVAL_URL — the evaluator-facing URL. eval-up.sh writes it into
# CORS_ALLOWED_ORIGINS on first run (plain HTTP over Traefik's :80 entrypoint;
# eval has no TLS / Let's Encrypt). Default http://localhost.
# EVAL_URL=http://localhost
#
# DEMO_SUPER_ADMIN_PASSWORD — pin the demo super-admin password so logins are
# reproducible across re-seeds. When unset under APP_ENV=dev/demo, seed_demo.py
# generates a random one and prints it ONCE (eval-up.sh surfaces it). Must be
# >= 12 chars. NEVER set this on a prod stack.
# Default below is a dev/demo convenience value applied to ALL seeded demo
# accounts (admin@, {frontend,backend,security}-admin@, [email protected])
# — clear it (or override in .env) for a prod deployment.
DEMO_SUPER_ADMIN_PASSWORD=DemoTest2026!
# ---------- Demo profile (low-cost public read-only + sandbox scan) ----------
# These knobs drive the low-cost public demo documented in
# Operator runbook (internal) — a read-only demo on a Hetzner CAX11 (Arm,
# 2 vCPU / 4 GB, ~$4/month) that also lets a visitor drive ONE bounded live
# scan or upload a BomLens SBOM against the seeded "Demo Sandbox" project.
#
# Bring the box up with the demo overlay ON TOP of the production stack:
# docker-compose -f docker-compose.yml -f docker-compose.demo.yml up -d
#
# The two SWITCHES below are what you set in `.env`. The SIZE / abuse rails
# (10 MiB input cap, 1 scan at a time, scancode/scanoss off, tighter trigger
# rate) are baked into docker-compose.demo.yml so they hold regardless of `.env`
# — you do not need to repeat them here.
#
# SECRETS (SECRET_KEY, POSTGRES_PASSWORD, the super-admin password) are NOT set
# here for the demo either: scripts/install.sh generates / prompts for them on
# the box. This block is only the demo behaviour toggles.
#
# 1) DEMO_READ_ONLY — the public-safety boundary. When true the backend rejects
# every state-changing HTTP request (POST/PUT/PATCH/DELETE) outside the auth
# allow-list with a friendly RFC 7807 403; reads are unaffected. Leave false
# for a normal writable install.
# DEMO_READ_ONLY=true
#
# 2) DEMO_ALLOW_SANDBOX_SCANS — opt-in carve-out to the read-only lock. Takes
# effect ONLY when DEMO_READ_ONLY is ALSO true. It permits exactly two writes
# against the seeded "Demo Sandbox" project so a visitor can run a bounded
# live scan / upload an SBOM:
# POST /v1/projects/{id}/scans
# POST /v1/projects/{id}/sbom-ingest
# Everything else stays 403'd. Fails CLOSED — only true/1/yes enable it, so a
# typo keeps the demo fully locked. The seed script materialises the "Demo
# Sandbox" project only when this is on (run `python -m scripts.seed_demo`).
# DEMO_ALLOW_SANDBOX_SCANS=true
#
# For reference, the size / abuse rails that docker-compose.demo.yml sets for
# you (documented here so you know what the demo runs with — DO NOT need to copy
# unless you deploy the demo WITHOUT the overlay):
# SCAN_SOURCE_RAW_DOWNLOAD_MAX_BYTES=10485760 # 10 MiB visitor-source cap
# SCAN_CONCURRENCY_CAP_PER_TEAM=1 # one scan at a time
# SCANCODE_ENABLED=false # drop heaviest license scanner
# SCANOSS_ENABLED=false # drop vendored-OSS scanner
# SCAN_TRIGGER_RATE_LIMIT=5/minute # tighter than prod's 20/minute
# SBOM_INGEST_MAX_BYTES=10485760 # 10 MiB SBOM-upload cap (prod 32 MiB)
# SBOM_INGEST_MAX_COMPONENTS=3000 # structural cap (prod 50,000)
#
# Postgres connection budget on the demo overlay (backend runs 2 uvicorn
# workers, one worker replica, max_connections=60):
# backend = 2 x (DB_POOL_SIZE 5 + DB_MAX_OVERFLOW 5) = 20
# worker = 1 x (DB_SYNC_POOL_SIZE 5 + DB_SYNC_MAX_OVERFLOW 5) = 10
# beat = 10 -> TOTAL ~40 + headroom, inside the 60 ceiling.
# ---------- Database ----------
# Two ways to configure the DB connection (resolved in core.config.database_url):
# 1. DATABASE_URL — full DSN, used by docker-compose dev/prod (default path).
# 2. DB_USER / DB_PASSWORD / DB_HOST / DB_NAME (+ optional DB_PORT) — composed
# at runtime. Used by the GCP Cloud Run module so DB_PASSWORD can be sourced
# from Secret Manager without templating the URL through Terraform.
# DATABASE_URL wins when both are set.
DATABASE_URL=postgresql+asyncpg://trustedoss:trustedoss@postgres:5432/trustedoss
# DB_USER=trustedoss
# DB_PASSWORD=trustedoss
# DB_HOST=postgres
# DB_PORT=5432
# DB_NAME=trustedoss
POSTGRES_USER=trustedoss
# POSTGRES_PASSWORD is the Postgres SUPERUSER (owner role) password. This is a
# PLACEHOLDER, not an operator choice: scripts/install.sh treats the value
# `trustedoss` (and other well-known defaults) as "regenerate" and writes a
# strong random password on a fresh install — a known superuser password would
# defeat the L1 runtime/owner role split. For a manual `docker-compose up`
# WITHOUT install.sh, set a real value here (dev compose falls back to
# `trustedoss` for local-only use). Avoid docker-compose-hostile characters
# ('$', whitespace) so the raw value and the DSN stay consistent.
# POSTGRES_PASSWORD=trustedoss
POSTGRES_DB=trustedoss
# Marathon bundle 8 (L1) — PostgreSQL role separation. When the
# trustedoss_app runtime role exists (provisioned by
# scripts/postgres-init.sh on first compose-up), Alembic uses
# DATABASE_URL_OWNER (= the trustedoss superuser) for DDL and the
# backend / worker runtime uses DATABASE_URL_APP (= trustedoss_app).
# audit_logs is INSERT-only for the runtime role, so even if the
# immutable trigger from migration 0012 is dropped, the runtime
# cannot tamper with the audit trail.
#
# Both env vars fall back to DATABASE_URL when unset (legacy /
# single-role / dev), so the L1 split is opt-in. install.sh wires
# both env vars on a fresh install.
# DATABASE_URL_OWNER=postgresql+asyncpg://trustedoss:<pw>@postgres:5432/trustedoss
# DATABASE_URL_APP=postgresql+asyncpg://trustedoss_app:<app_pw>@postgres:5432/trustedoss
# POSTGRES_APP_USER=trustedoss_app
# POSTGRES_APP_PASSWORD=
# ---------- Redis / Celery ----------
REDIS_URL=redis://redis:6379/0
# Celery worker scaling (work-B / B2). Two independent knobs:
# CELERY_CONCURRENCY — parallel task slots INSIDE one worker process.
# Each slot can fork a full cdxgen/scancode/Trivy/dotnet
# pipeline (~1.5-2 GB transient), so keep this modest.
# WORKER_REPLICAS — number of worker CONTAINERS. Prefer adding replicas
# over raising concurrency. Honoured by `deploy.replicas`
# ONLY under Docker Swarm; under plain docker-compose use
# `docker-compose -f docker-compose.yml up -d --scale worker=N`.
# CELERY_CONCURRENCY=2
# WORKER_REPLICAS=1
# WORKER_CPU_LIMIT — CPU cap for the worker container (docker-compose.yml
# `deploy.resources.limits.cpus`). Compose V2 REJECTS a
# value greater than the host's online CPU count at `up`,
# so on a small box (e.g. a 2-vCPU host) lower this or let
# install.sh set it for you (it clamps to the CPU count).
# Unset defaults to 4.0.
# WORKER_CPU_LIMIT=4.0
# ---------- DB connection pool (B1 — concurrency / stability) ----------
# Per-process pool sizing. Total connections opened by ONE process =
# pool_size + max_overflow. Read at engine-construction time, so a change
# takes effect on the next process start (no rebuild). The backend clamps
# each value (floor + ceiling) so a typo cannot exhaust Postgres.
#
# FastAPI (async, asyncpg) — serves request handlers, gets the larger pool.
# DB_POOL_SIZE=20
# DB_MAX_OVERFLOW=10
# DB_POOL_TIMEOUT=30 # seconds a request waits for a free connection
# DB_POOL_RECYCLE=1800 # recycle a connection after N seconds (-1 = off)
#
# Celery (sync, psycopg2) — applies per worker AND per beat process.
# DB_SYNC_POOL_SIZE=5
# DB_SYNC_MAX_OVERFLOW=5
# DB_SYNC_POOL_TIMEOUT=30
# DB_SYNC_POOL_RECYCLE=1800
#
# ---- Postgres connection budget (sizing formula) -------------------------
# Keep the whole fleet under Postgres `max_connections` (default 100):
#
# backend_conns = uvicorn_workers x (DB_POOL_SIZE + DB_MAX_OVERFLOW)
# worker_conns = WORKER_REPLICAS x (DB_SYNC_POOL_SIZE + DB_SYNC_MAX_OVERFLOW)
# beat_conns = 1 x (DB_SYNC_POOL_SIZE + DB_SYNC_MAX_OVERFLOW)
# TOTAL = backend_conns + worker_conns + beat_conns + ~5 admin/headroom
# require: TOTAL <= Postgres max_connections
#
# Worked example with the prod defaults (backend image runs 4 uvicorn workers):
# backend = 4 x (20 + 10) = 120 -> already over 100 by itself!
# So for a single-host default deployment EITHER drop DB_POOL_SIZE/MAX_OVERFLOW
# (e.g. 10+5 -> backend 60), OR raise Postgres max_connections (postgres
# command: `-c max_connections=300`) and provision RAM accordingly (~10 MB per
# connection). Recommended single-host starting point:
# DB_POOL_SIZE=10 DB_MAX_OVERFLOW=5 (backend 4x15 = 60)
# DB_SYNC_POOL_SIZE=5 DB_SYNC_MAX_OVERFLOW=5 WORKER_REPLICAS=2 (worker 2x10=20, beat 10)
# -> TOTAL = 60 + 20 + 10 + 5 = 95 (fits default max_connections=100)
# Scale Postgres max_connections up before scaling backend uvicorn workers or
# WORKER_REPLICAS past this budget.
# ---------- Scan-trigger abuse controls (B1) ----------
# Per-USER rate limit on POST /v1/projects/{id}/scans (slowapi "<n>/<period>").
# Keyed by access-token sub, so NAT'd users / CI runners don't share a bucket.
# 429 + RFC 7807 + Retry-After when exceeded.
# SCAN_TRIGGER_RATE_LIMIT=20/minute
#
# Per-TEAM concurrent (queued+running) scan cap — protects the shared Celery
# worker pool from one team's burst. 429 with RFC 7807 `limit` extension field
# + Retry-After. 0 (or negative) disables the cap.
# SCAN_CONCURRENCY_CAP_PER_TEAM=10
# ---------- Scan stability (A1) ----------
# Soft time limit (seconds) for scan tasks. On expiry the worker raises
# SoftTimeLimitExceeded inside the task, which cleans up the workspace and
# marks the scan failed with a clear message. Primary timeout mechanism.
# SCAN_SOFT_TIME_LIMIT_SECONDS=3600
# Hard time limit (seconds, SIGKILL) — last-resort backstop above the soft
# limit. The backend ENFORCES hard > soft: if you set this <= the soft limit
# (typo, or swapping the two vars), the effective hard limit is clamped to
# soft + 60s so the soft-limit cleanup handler always runs before SIGKILL.
# (The +60s grace is a backend constant, not an env knob.)
# SCAN_HARD_TIME_LIMIT_SECONDS=3900
# Grace period (seconds) before the workspace orphan cleaner reclaims a
# terminal scan's workspace directory. A workspace dir with NO owning scan row
# needs DOUBLE this grace (2x) before reclaim, so a scan about to start is
# never deleted out from under itself.
# WORKSPACE_ORPHAN_MAX_AGE_SECONDS=900
# ---------- Scan retention (DT-style ref-keyed) ----------
# CI/webhook triggers a scan per PR merge / push. To stop the scans table (and
# its findings) growing forever, the scan_retention beat keeps only the latest
# succeeded scan per (project, ref) plus any scan with an explicit
# metadata.release label; the rest are reclaimed. Disk artifacts have their own
# sweepers (scan_source_cleaner / workspace_cleaner) — this is the DB side.
# Days a superseded snapshot is kept before hard delete (cascade removes its
# findings). 0 = reclaim on the next beat tick.
# SCAN_RETENTION_SUPERSEDED_GRACE_DAYS=7
# Per project, the newest N reclaimable scans (ref-less succeeded + all
# failed/cancelled) are always kept regardless of age.
# SCAN_RETENTION_KEEP_LAST=30
# Age past which reclaimable scans beyond KEEP_LAST are deleted.
# SCAN_RETENTION_MAX_AGE_DAYS=180
# ---------- scancode first-party license detection (A2) ----------
# scancode runs over the cloned first-party source tree to produce DETECTED
# licenses (third-party dependency licenses stay DECLARED, from cdxgen). All
# four guards are read at runtime by the worker.
# Hard wall-clock limit (seconds) for one scancode invocation.
# SCANCODE_TIMEOUT_SECONDS=600
# Max eligible first-party files before the detection stage is skipped (a
# pre-scan walk excludes node_modules / vendor / .git / dist / build / target
# / .venv …, so vendored trees do not count toward this ceiling).
# SCANCODE_MAX_FILES=20000
# Max detected license findings persisted from one scan (caps license_findings
# rows; excess detections are dropped with a WARNING — the scan still succeeds).
# SCANCODE_MAX_DETECTIONS=5000
# Max size (bytes) of the scancode JSON result we will deserialize (OOM guard).
# Over the ceiling we skip parsing (no detected licenses; declared cdxgen
# licenses still stand). 256 MiB.
# SCANCODE_MAX_RESULT_BYTES=268435456
# ---------- scanoss vendored-OSS identification (BomLens parity Phase J) -------
# scanoss-py fingerprints first-party source files and matches them against the
# OSS Knowledge Base to surface third-party code COPIED into the tree without a
# package manifest (complements cdxgen's declared deps + scancode's license
# detection). The worker image ALWAYS ships the CLI (MIT); these knobs gate and
# tune it. Default OFF. Privacy: when enabled, scanoss sends file FINGERPRINTS
# (hashes) — never source — to SCANOSS_API_URL, so leave off (or point at a
# private mirror) for air-gapped / sensitive codebases.
SCANOSS_ENABLED=false
# OSS Knowledge Base API endpoint. Default is the free public SCANOSS service;
# override for a self-hosted / on-prem SCANOSS server or a premium endpoint.
SCANOSS_API_URL=https://api.osskb.org
# Optional API key for the premium / authenticated SCANOSS endpoint. Blank uses
# the free public tier (rate-limited).
SCANOSS_API_KEY=
# Wall-clock cap (seconds) for one scanoss invocation.
SCANOSS_TIMEOUT_SECONDS=300
# ---------- Tool log streaming (P2 #8c — cdxgen / scancode WS panel) ----------
# Per-line and per-scan caps for the live tool-log stream the scan drawer
# renders. The worker forwards every cdxgen / scancode stdout/stderr line over
# the existing /ws/scans/<id> channel; these guards bound wire volume + RAM.
#
# Max length (chars) of a single streamed log line. Over this we truncate with
# a `…(truncated)` marker. Default 2000.
# SCAN_LOG_LINE_MAX_LEN=2000
#
# Max lines streamed for a single scan (shared across cdxgen + scancode). Past
# this many lines we silently drop further publishes for that scan; the
# subprocess keeps running. Set to 0 to disable streaming entirely (kill
# switch). Default 5000.
# SCAN_LOG_MAX_LINES_PER_SCAN=5000
# ---------- Dynamic scan executor (BomLens-style per-environment images) -------
# How the SBOM-generation stage (build-prep + cdxgen) runs:
# inprocess (default) — worker-local subprocesses, exactly as before.
# local_docker — on-prem: launch a per-environment cdxgen sidecar (e.g.
# the Android SDK image) via the host Docker socket. The
# socket is a host-escape surface — on-prem / single-tenant
# only. Requires the worker to have the Docker CLI + the
# socket mounted (see docker-compose). Falls back to
# inprocess for any non-routed environment or a missing CLI.
# SCAN_EXECUTOR=inprocess
#
# Which detected environments route to a sidecar (comma-separated). Default
# `android` — the one verified detection gap (no Android SDK in the worker → 0→67
# components). node/go/rust/ruby/java/python/php/dotnet resolve IDENTICALLY in the
# all-in-one worker (Colima-verified), so route them only for per-build isolation,
# not detection gain. Widen e.g.:
# SCAN_LOCAL_DOCKER_ENVS=android,node,go,rust,ruby,java,python,php,dotnet
# SCAN_LOCAL_DOCKER_ENVS=android
#
# How the sidecar reaches the workspace volume (SECURITY-sensitive):
# named (DEFAULT, secure) — mount ONLY SCAN_WORKSPACE_VOLUME at SCAN_WORKSPACE_MOUNT.
# Requires SCAN_WORKSPACE_VOLUME; unset → falls back to
# in-process (the sidecar sees the scan tree and nothing else).
# volumes_from (DANGEROUS) — --volumes-from re-mounts EVERY worker volume, incl. the
# cosign signing key, into the untrusted build. Refused
# unless SCAN_VOLUMES_FROM_ACK=1 + SCAN_WORKER_CONTAINER set.
# SCAN_DOCKER_VOLUME_STRATEGY=named
# SCAN_WORKSPACE_VOLUME= # REQUIRED for named: the workspace volume name
# # (compose-prefixed, e.g. trustedoss-portal_scan-workspace)
# SCAN_WORKSPACE_MOUNT=/tmp/trustedoss # mount point (prod: /workspace)
# SCAN_WORKER_CONTAINER= # REQUIRED for volumes_from: explicit worker container ref
# SCAN_VOLUMES_FROM_ACK= # set 1 to accept the volumes_from over-share (not recommended)
#
# Sidecar resource bounds (default ON — an untrusted build must not OOM the host):
# SCAN_SIDECAR_PIDS_LIMIT=4096
# SCAN_SIDECAR_MEMORY=4g
# SCAN_SIDECAR_CPUS=2
# SCAN_SIDECAR_CAP_DROP=ALL # drop all caps, add back the minimal set
# SCAN_SIDECAR_CAP_ADD=CHOWN,DAC_OVERRIDE,FOWNER,SETGID,SETUID
# SCAN_SIDECAR_NETWORK= # isolated egress network (recommended); unset = default
# # bridge with a startup WARNING (can reach internal services)
#
# Per-environment image selection (mirrors BomLens source-detect.sh):
# CDXGEN_IMAGE_TAG=v12 # cdxgen language-image tag
# CDXGEN_ALLINONE_IMAGE=ghcr.io/cyclonedx/cdxgen:v12.5.0 # mixed/unknown
# SCAN_ANDROID_IMAGE_PREFIX=ghcr.io/sktelecom/sbom-scanner-android-sdk
# SCAN_ANDROID_IMAGE_TAG=v1.0.0 # PIN a semver or sha256:<digest> (rule #9).
# # :latest is refused unless allowed below.
# SCAN_ALLOW_UNPINNED_IMAGE= # set 1 to allow :latest (dev only)
# SCAN_ANDROID_API_DEFAULT=34 # fallback compileSdk
#
# cdxgen output toggles (apply to both inprocess + sidecar):
# CDXGEN_SPEC_VERSION=1.5 # set 1.6 to emit CycloneDX 1.6
# CDXGEN_FETCH_LICENSE=false # true → cdxgen resolves component licenses (slower)
#
# Post-cdxgen license enrichment. When cdxgen emits a component with
# no SPDX license — the common case for a bare requirements.txt / go.mod with
# no installed packages — the pipeline asks the component's PUBLIC registry
# (PyPI / Maven Central / crates.io / pkg.go.dev / RubyGems / NuGet) for the declared license by
# purl and caches it. This is what pulls the "unknown" license ratio down.
# Default ON (only a package name+version leaves the network, to the same
# registry the package manager already contacts). Set false on an AIR-GAPPED
# deployment so an unlicensed component stays unknown instead of paying a
# per-component network timeout.
# LICENSE_FETCH_ENABLED=true
# ---------- DB connection pool (B1 — concurrency / stability) ----------
# Per-process pool sizing. total connections per process = pool_size +
# max_overflow. Multiply the FastAPI numbers by the uvicorn worker count and
# add the Celery sync numbers (per worker process) to stay under Postgres
# `max_connections` (default 100). Read at engine-construction time, so a
# change takes effect on the next process start (no rebuild).
#
# FastAPI (async, asyncpg) — serves request handlers, gets the larger pool.
# DB_POOL_SIZE=20
# DB_MAX_OVERFLOW=10
# DB_POOL_TIMEOUT=30 # seconds a request waits for a free connection
# DB_POOL_RECYCLE=1800 # recycle a connection after N seconds (-1 = off)
#
# Celery (sync, psycopg2) — worker concurrency is low, so a smaller pool.
# DB_SYNC_POOL_SIZE=5
# DB_SYNC_MAX_OVERFLOW=5
# DB_SYNC_POOL_TIMEOUT=30
# DB_SYNC_POOL_RECYCLE=1800
# ---------- Scan-trigger abuse controls (B1) ----------
# Per-USER rate limit on POST /v1/projects/{id}/scans (slowapi "<n>/<period>").
# Keyed by access-token sub, so NAT'd users / CI runners don't share a bucket.
# 429 + RFC 7807 + Retry-After when exceeded.
# SCAN_TRIGGER_RATE_LIMIT=20/minute
#
# Per-TEAM concurrent (queued+running) scan cap — protects the shared Celery
# worker pool from one team's burst. 429 with RFC 7807 `limit` extension field
# + Retry-After. 0 (or negative) disables the cap.
# SCAN_CONCURRENCY_CAP_PER_TEAM=10
# ---------- Auth ----------
SECRET_KEY=change-this-to-a-random-secret-key-min-32-chars
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7
# Optional keyed-hash key for audit_logs.diff.provider_user_id_hash
# (Marathon bundle 4 — T / L4). With the key set, the audit log uses
# keyed BLAKE2b instead of bare SHA-256 — defeats dictionary-attack
# enumeration of provider user ids. Unset → legacy SHA-256 (preserves
# cross-rotation comparability of existing audit rows). In APP_ENV=prod
# the legacy path emits a structured WARNING per call so operators see
# the signal that the hardening is opted out.
# Minimum length 16 bytes — shorter keys are refused with RuntimeError
# so an operator typo fails loudly instead of degrading entropy.
# Generate via: python3 -c 'import secrets; print(secrets.token_hex(32))'
# AUDIT_HASH_KEY=
# W6-#43a (ADR-0001): the Dependency-Track section was removed when DT was
# replaced by Trivy. Vulnerability matching now runs entirely from
# the worker via ``trivy sbom`` against the locally-cached Trivy DB (no
# external service, no API key). See the W6-#42 vulnerability rematch beat
# for the replacement data path. GATE_EPSS_THRESHOLD below is still honoured — the EPSS score
# now rides on the Trivy-emitted Vulnerability rows.
# ---------- Build / policy gate ----------
# Optional EPSS dimension for the CI build gate. EPSS (Exploit Prediction
# Scoring System) is the probability (0..1) a CVE is exploited in the next 30
# days. UNSET (the default) = DISABLED: the gate keeps only the existing
# Critical-CVE / forbidden-license conditions. When set to a value in 0..1, the
# gate ALSO fails the build if any open finding has epss_score >= the threshold,
# and the gate result reports epss_gate_count + epss_threshold. Findings with no
# EPSS value never trip the gate (a missing score cannot satisfy >=). EPSS data
# is collected during the DT resync, so only CVEs DT supplies a value for count.
# GATE_EPSS_THRESHOLD=0.5
#
# Optional reachability RELAXATION for the critical-CVE gate condition. GLOBAL,
# default OFF (legacy behaviour: ANY open critical fails the build). When set to
# a truthy value (1/true/yes/on), the gate stops failing the build for open
# critical CVEs the reachability analyser has PROVEN are not on a real call path
# (reachable=false). This is an opt-in, operator-chosen relaxation — it can only
# SHRINK the set of blocking criticals, never strengthen the gate.
#
# Go-only limitation + SAFE-BY-DEFAULT FALLBACK: reachability analysis is
# currently Go-only, so findings in other ecosystems (npm/PyPI/Maven/...) stay
# "not analysed" (reachable=NULL). To avoid silently disabling the gate on those
# ecosystems, the relaxation applies ONLY to scans whose open criticals were
# actually reachability-analysed (at least one finding with a non-NULL verdict).
# On an un-analysed scan the relaxation is a NO-OP and the gate blocks on the
# FULL set of open criticals, exactly as if the flag were off. Even when it
# applies, only PROVEN-unreachable (reachable=false) criticals are excluded;
# not-analysed (NULL) criticals always keep blocking. When the relaxation
# actually suppresses a blocking critical, the portal emits a WARNING log
# (policy_gate.reachable_relaxation_suppressed_criticals).
# GATE_REACHABLE_CRITICAL_ONLY=false
# Maximum lifetime (in days) of a waiver on a FORBIDDEN license. A waiver relaxes
# the build gate, so a forbidden-license waiver must carry an expiry and may not
# outlive this cap — the API rejects a forbidden waiver with no expiry, or one
# whose expires_at is further out than LICENSE_WAIVE_MAX_DAYS, with a 422.
# Waivers on conditional / allowed licenses are unaffected (they may be
# indefinite). Read at request time; unset / unparseable / non-positive → 90.
# NOTE: the cap is enforced when a waiver is CREATED — lowering it later does not
# retroactively shorten waivers already written within the old cap; each runs to
# its own expires_at.
# LICENSE_WAIVE_MAX_DAYS=90
# ---------- Malicious packages (#26) ----------
# Master switch for the malicious-package signal. When on, every component seen
# by a scan is checked against the OSV MAL- snapshot that ships with the release
# and gets a verdict (flagged / clear). Read at request time; default ON.
# MALICIOUS_ENABLED=true
# Whether a flagged package FAILS the build gate. This axis is default-ON, unlike
# the EPSS and reachability relaxations above, because a malicious package is not
# a risk to weigh — the published artifact is the attack. Turning it off leaves
# the verdicts visible in the UI but stops them blocking. Default ON.
# GATE_MALICIOUS_ENABLED=true
# Maximum lifetime (in days) of a waiver on a malicious finding. Shorter than the
# license cap on purpose: a malicious waiver is a decision to keep shipping a
# known-bad artifact, which is only ever a bridge to removing it. The API rejects
# a malicious waiver with no expiry, or one reaching past this cap, with a 422.
# Enforced at creation — lowering it later does not shorten existing waivers.
# Unset / unparseable / non-positive -> 30.
# MALICIOUS_WAIVE_MAX_DAYS=30
# Whether the weekly beat REBUILDS the snapshot from OSV over the network.
# Default OFF: this is outbound traffic an operator has not asked for, and the
# snapshot already ships with each release. The other half of the beat — re-
# checking stock components against the snapshot on hand — runs either way, so
# an air-gapped install still picks up a newer snapshot from an upgrade without
# ever reaching the internet.
# MALICIOUS_REFRESH_ENABLED=false
# Age (in days) past which admin/health calls the snapshot stale. Much shorter
# than the end-of-life snapshot's window (180) because malicious advisories are
# published daily, so a two-month-old snapshot has stopped answering the question
# it is asked. Unset / unparseable / non-positive -> 60.
# MALICIOUS_SNAPSHOT_STALE_DAYS=60
# ---------- Workspace ----------
WORKSPACE_HOST_PATH=/opt/trustedoss/workspace
# ---------- scancode first-party license detection (PR-A2) ----------
# scancode runs over the cloned first-party source tree to produce DETECTED
# licenses (third-party dependency licenses stay DECLARED, from cdxgen). The
# ORT evaluator was removed in PR-A2. All three guards are read at runtime.
# Hard wall-clock limit (seconds) for one scancode invocation.
# SCANCODE_TIMEOUT_SECONDS=600
# Max eligible first-party files before the detection stage is skipped (a
# pre-scan walk excludes node_modules / vendor / .git / dist / build / target
# / .venv …, so vendored trees do not count toward this ceiling).
# SCANCODE_MAX_FILES=20000
# Max detected license findings persisted from one scan (caps license_findings
# rows; excess detections are dropped with a WARNING — the scan still succeeds).
# SCANCODE_MAX_DETECTIONS=5000
# Max size (bytes) of the scancode JSON result we will deserialize. The result
# is keyed off the attacker-controlled tree and json.load materialises the whole
# document, so an unbounded result is an OOM vector — over the ceiling we skip
# parsing (no detected licenses, declared cdxgen licenses still stand). 256 MiB.
# SCANCODE_MAX_RESULT_BYTES=268435456
# Native-lib path hints. The bundled worker image sets these so scancode's
# ctypes loader finds a complete system libarchive/libmagic (the bundled
# aarch64 wheels are empty stubs → "undefined symbol: archive_read_new" on
# arm64). Override ONLY if you run scancode from a custom image where the libs
# live elsewhere.
# EXTRACTCODE_LIBARCHIVE_PATH=/usr/local/lib/scancode-libarchive.so
# TYPECODE_LIBMAGIC_PATH=/usr/local/lib/scancode-libmagic.so
# ---------- Scan stability (PR-A1) ----------
# Soft time limit (seconds) for scan tasks. On expiry the worker raises
# SoftTimeLimitExceeded inside the task, which cleans up the workspace and
# marks the scan failed with a clear message. Primary timeout mechanism.
# SCAN_SOFT_TIME_LIMIT_SECONDS=3600
# Hard time limit (seconds, SIGKILL) — last-resort backstop above the soft
# limit for a task that ignores the soft signal. The app ENFORCES hard > soft:
# if you set this <= the soft limit (e.g. a typo, or swapping the two vars),
# the effective hard limit is clamped to soft + 60s so the soft-limit cleanup
# handler always runs before SIGKILL.
# SCAN_HARD_TIME_LIMIT_SECONDS=3900
# Grace period (seconds) before the workspace orphan cleaner reclaims a
# terminal scan's workspace directory. Avoids racing the worker's own
# finally: rmtree right after the scan flips terminal. A workspace dir with NO
# owning scan row (deleted scan, or an INSERT still in flight) needs DOUBLE
# this grace (2x) before reclaim, so a scan about to start is never deleted.
# WORKSPACE_ORPHAN_MAX_AGE_SECONDS=900
# ---------- Scan stability (PR-A1) ----------
# Soft time limit (seconds) for scan tasks. On expiry the worker raises
# SoftTimeLimitExceeded inside the task, which cleans up the workspace and
# marks the scan failed with a clear message. Primary timeout mechanism.
# SCAN_SOFT_TIME_LIMIT_SECONDS=3600
# Hard time limit (seconds, SIGKILL) — last-resort backstop above the soft
# limit for a task that ignores the soft signal. The app ENFORCES hard > soft:
# if you set this <= the soft limit (e.g. a typo, or swapping the two vars),
# the effective hard limit is clamped to soft + 60s so the soft-limit cleanup
# handler always runs before SIGKILL.
# SCAN_HARD_TIME_LIMIT_SECONDS=3900
# Grace period (seconds) before the workspace orphan cleaner reclaims a
# terminal scan's workspace directory. Avoids racing the worker's own
# finally: rmtree right after the scan flips terminal. A workspace dir with NO
# owning scan row (deleted scan, or an INSERT still in flight) needs DOUBLE
# this grace (2x) before reclaim, so a scan about to start is never deleted.
# WORKSPACE_ORPHAN_MAX_AGE_SECONDS=900
# ---------- Source archive upload (feat/zip-upload) ----------
# Limits for the "scan a local .zip" upload path. All are read at runtime, so
# changing one needs only a worker/backend restart (no rebuild). Defaults are
# the same constants the code falls back to — listed here for operator
# visibility and so a hardened deployment can tighten them.
#
# Hard ceiling on the COMPRESSED upload size (one .zip). Streamed-bytes
# enforced; the endpoint also early-rejects on Content-Length. Default 100 MiB.
# SOURCE_ARCHIVE_MAX_BYTES=104857600
# Per-PROJECT cumulative storage budget across all saved .zip archives. Blocks
# a low-privilege developer from looping uploads to exhaust the workspace
# volume (disk DoS). Saving past this returns 507. Default 500 MiB.
# SOURCE_ARCHIVE_PROJECT_QUOTA_BYTES=524288000
# Hard ceiling on total UNCOMPRESSED bytes across all members (zip-bomb guard,
# counted on actual extracted bytes, not the declared header). Default 1 GiB.
# SOURCE_ARCHIVE_MAX_EXTRACTED_BYTES=1073741824
# Hard ceiling on the number of entries in one archive. Default 50000.
# SOURCE_ARCHIVE_MAX_MEMBERS=50000
# Per-member uncompressed/compressed ratio ceiling (zip-bomb guard). A member
# declaring 0 compressed bytes for >0 uncompressed is always rejected.
# Default 200x.
# SOURCE_ARCHIVE_MAX_COMPRESSION_RATIO=200
# Uncompressed-size floor below which the ratio ceiling is NOT applied. Real
# OSS trees ship tiny sparse fixtures that compress 900x+ but inflate to a few
# hundred KB (harmless); only members declaring more than this many
# uncompressed bytes are ratio-checked. The total-extracted cap still applies
# to every member. Default 10 MiB.
# SOURCE_ARCHIVE_RATIO_GUARD_MIN_BYTES=10485760
# Age (hours) past which an UNREFERENCED uploaded archive is reclaimable by the
# 6-hourly retention beat. An archive referenced by a queued/running scan is
# never swept regardless of age. Default 24h.
# SOURCE_ARCHIVE_RETENTION_HOURS=24
# ---------- Backups (Marathon bundle 3 — D2) ----------
# Output directory for `tasks.backup`. Mounted from the host in
# docker-compose.dev.yml / docker-compose.yml. The admin API lists from
# the same root.
# BACKUPS_ROOT=/opt/trustedoss/backups
# Retention window (days) for `auto-*` backups. Manual backups are
# never auto-pruned.
# BACKUP_RETENTION_DAYS=7
# Per-call subprocess timeout (seconds) for pg_dump and psql restore.
# Bump for very large databases (default suits up to a few hundred GB).
# BACKUP_SUBPROCESS_TIMEOUT=3600
# Offsite backup (optional) — scripts/backup-offsite.sh pushes local backups to
# an rclone remote (Cloudflare R2 / Backblaze B2 / any S3-compatible). Unset =
# disabled (local-only). Requires rclone installed + `rclone config`. See
# See the operator runbook (internal).
# BACKUP_OFFSITE_REMOTE=r2:trustedoss-backups
# Only copy backups newer than this each run (default 25h). Any rclone duration.
# BACKUP_OFFSITE_MAX_AGE=25h
# ---------- W6-#42 Vulnerability re-match beat ----------
# The rematch beat re-runs ``trivy sbom`` against every succeeded scan's
# preserved CycloneDX SBOM on a 6-hourly schedule and surfaces new / elevated
# critical+high CVEs as notifications. This is the W6 promotion of the
# Dependency-Track "rematch on DB update" feature (DT was removed by ADR-0001;
# see ``docs/decisions/0001-replace-dt-with-trivy.md``).
#
# Minimum hours between rematch runs for a given scan. Default 6h — paired
# with Trivy's weekly DB refresh cadence keeps the per-scan latency between an
# upstream NVD publish and a new finding under a day. Bounded [1, 168].
# VULN_REMATCH_INTERVAL_HOURS=6
# Max scans the beat enqueues in one tick. Default 50. Bounded [1, 5000].
# Tune up on small corpora, down if the worker pool is shared with long scans.
# VULN_REMATCH_BATCH_SIZE=50
# Slack (seconds) between the beat's "due" cutoff and the per-scan
# SELECT FOR UPDATE SKIP LOCKED so a scan that was JUST rematched by another
# worker is not immediately re-enqueued. Default 30s. Bounded [0, 600].
# VULN_REMATCH_LOCK_SKEW_SECONDS=30
# ---------- X1 Vulnerability SLA / aging tracking ----------
# Per-severity remediation SLA windows, in days, measured from the finding's
# PROJECT-level first detection (carried forward across re-scans/re-matches).
# The vulnerabilities list computes sla_due_date / sla_status (overdue |
# imminent | ok) from these; "imminent" means the due date is within 7 days.
# info/unknown severities carry no SLA. Non-numeric or <= 0 values fall back
# to the defaults below.
# VULN_SLA_DAYS_CRITICAL=7
# VULN_SLA_DAYS_HIGH=30
# VULN_SLA_DAYS_MEDIUM=90
# VULN_SLA_DAYS_LOW=180
# Daily SLA-breach sweep (02:45 UTC beat): notifies every member of a
# project's owning team (one in-app alert per project) when open findings
# cross their SLA due date within the last 24h window. Default on (internal
# computation, no egress); only false/0/no disable.
# VULN_SLA_ALERTS_ENABLED=true
# ---------- W6-#44 Trivy DB lifecycle (worker bootstrap + weekly refresh) ----
# The worker downloads ``trivy --download-db-only`` once on boot (background
# thread, doesn't block worker startup) and refreshes weekly via a Celery
# beat. Worker boot fires a Celery ``worker_ready`` signal handler; the
# weekly beat runs ``trustedoss.trivy_db_refresh`` at Sunday 03:00 UTC by
# default. Both honour the operator knobs below.
#
# OCI repository the DB is pulled from. Override for an air-gapped mirror
# (Harbor / Nexus / Artifactory / private GHCR). See
# docs-site/docs/admin-guide/vulnerability-data.md §"Air-gapped operation".
# TRIVY_DB_REPOSITORY=ghcr.io/aquasecurity/trivy-db
# Hours between refresh ticks the admin/health panel reports as the
# configured cadence. The actual beat schedule is Sunday 03:00 UTC; this
# value is informational + used by ``next_refresh_at`` in the panel.
# Bounded by Trivy's upstream rebuild cadence (~6h floor) and the operator's
# tolerance for staleness (no enforced ceiling).
# TRIVY_DB_REFRESH_HOURS=168
# Worker-container path where Trivy unpacks the DB. Should be a mounted
# volume so reboots don't re-download. The worker image leaves this unset
# (Trivy default: $HOME/.cache/trivy); production deployments often mount
# /var/lib/trivy from a persistent volume.
# TRIVY_CACHE_DIR=
# Whether the worker should download the DB on boot. ``true`` (default) is
# the only sensible setting for online deployments. Set ``false`` on
# air-gapped clusters where the DB is mirrored to a shared volume by a
# separate process so the worker never attempts a network pull.
# TRIVY_DB_BOOTSTRAP_ON_START=true
# Wall-clock cap (seconds) on the boot-time download subprocess. The first
# download is ~500 MiB and typically completes in 1-3 minutes on a fast
# link; 15 minutes absorbs a slow corporate proxy. Bounded [30, 3600].
# TRIVY_DB_BOOTSTRAP_TIMEOUT_SECONDS=900
# Wall-clock cap (seconds) on the weekly beat refresh subprocess.
# Incremental refreshes are typically faster than the bootstrap, but we
# keep symmetric caps. Bounded [30, 3600].
# TRIVY_DB_REFRESH_TIMEOUT_SECONDS=900
# ---------- CISA KEV catalog refresh (daily beat) ----------
# A daily Celery beat (01:45 UTC) downloads CISA's public Known Exploited
# Vulnerabilities feed and flags matching catalog CVEs (kev / kev_date_added /
# kev_due_date). The KEV signal backs the Vulnerabilities tab's
# ``sort=priority`` ranking (KEV -> severity -> EPSS).
#
# Feed URL. Override to point at an internal mirror (same idea as
# TRIVY_DB_REPOSITORY for air-gapped clusters).
# KEV_FEED_URL=https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json
# Whether the beat fetches the feed at all. Set false on air-gapped
# deployments with no mirror — the beat then logs a skip and makes no
# network attempt (existing KEV flags are left as-is).
# KEV_REFRESH_ENABLED=true
# HTTP timeout (seconds) for the feed download (~10 MiB from a CDN).
# Bounded [1, 600].
# KEV_REFRESH_TIMEOUT_SECONDS=30
# ---------- Notifications ----------
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SLACK_WEBHOOK_URL=
TEAMS_WEBHOOK_URL=
# ---------- OAuth (demo SaaS only) ----------
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# ---------- GitHub App (v2.2-b1 — credential storage + token minting) ----------
# Fernet key (URL-safe base64, 32 bytes) used to encrypt stored GitHub App
# private keys / webhook secrets AT REST. Generate with:
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# In dev/CI, if this is unset a key is DERIVED from SECRET_KEY so the stack works
# out of the box (a WARNING is logged). In PRODUCTION this MUST be set: with
# APP_ENV=prod and no key, the app FAILS CLOSED (refuses to encrypt/decrypt
# credentials) rather than silently binding them to the JWT secret's blast radius.
# ROTATION: changing this key currently requires RE-REGISTERING every stored
# credential — the new key cannot decrypt rows written under the old one. Rolling
# rotation (accept old keys for decrypt via MultiFernet) is a tracked follow-up.
GITHUB_APP_ENCRYPTION_KEY=
# GitHub REST API base. Override for GitHub Enterprise Server (e.g.
# https://github.example.com/api/v3). Default is the public API.
# PRODUCTION policy (enforced when APP_ENV=prod): MUST be https:// and MUST NOT
# point at an internal host — loopback (127.0.0.0/8, ::1), link-local incl. the
# cloud metadata IP 169.254.169.254, or RFC-1918 private ranges (10/8, 172.16/12,
# 192.168/16) are rejected (SSRF / cleartext guard). In dev/CI any scheme/host is
# allowed so local GitHub Enterprise and tests work.
GITHUB_API_URL=https://api.github.com
# ---------- cosign SBOM signing (v2.3-s1) ----------
# After a source scan generates the CycloneDX SBOM, the worker signs it with
# cosign so a downstream consumer can verify integrity + provenance. Signing is
# BEST-EFFORT: if cosign is absent or no key is configured the scan still
# succeeds (unsigned, with a WARNING). The worker image ships the cosign binary.
#
# D2 decision: KEY-BASED is the DEFAULT (self-hosted / on-prem / air-gapped is
# the first-class target). KEYLESS (OIDC; Fulcio/Rekor) is opt-in via the toggle.
#
# Toggle: false (default) = key-based; true = keyless OIDC. When unset → key-based.
COSIGN_KEYLESS=false
#
# --- Key-based path (default) ---
# Generate a key pair with `bash scripts/cosign-keygen.sh`, mount cosign.key
# into the worker (see COSIGN_KEYS_HOST_PATH below), and point COSIGN_KEY_PATH at
# the in-container path. cosign.pub is distributed to verifiers (not needed here).
COSIGN_KEY_PATH=
# The private-key PASSWORD, ENCRYPTED with the app Fernet key (core.crypto) —
# NEVER stored in cleartext. Produce the ciphertext with (run in the worker so it
# uses the app's encryption key):
# docker-compose run --rm worker \
# python -c "import sys;from core.crypto import encrypt_secret;print(encrypt_secret(sys.argv[1]))" 'YOUR_PASSWORD'
# Leave blank for a passwordless key. The plaintext password is decrypted only at
# signing time and handed to cosign via the COSIGN_PASSWORD subprocess env —
# never on the command line, never logged. NOTE: this reuses GITHUB_APP_ENCRYPTION_KEY
# (above) as the Fernet key — the same prod fail-closed / rotation caveats apply.
COSIGN_KEY_PASSWORD_ENCRYPTED=
# Host directory holding cosign.key / cosign.pub. docker-compose mounts it
# read-only into the worker at /cosign (so COSIGN_KEY_PATH=/cosign/cosign.key).
COSIGN_KEYS_HOST_PATH=./secrets/cosign
#
# --- Keyless path (opt-in: COSIGN_KEYLESS=true) ---
# cosign drives its own OIDC identity (ambient CI token / configured provider)
# and uploads to Rekor. No key pair needed. For a private Sigstore deployment,
# set COSIGN_OIDC_ISSUER / SIGSTORE_FULCIO_URL / SIGSTORE_REKOR_URL in the
# worker env (these are forwarded by integrations/_subprocess_env.py).
#
# Per-invocation wall-clock cap (seconds). Key-based is sub-second; keyless adds
# an OIDC + Rekor round-trip. Read at call time (rule #11).
COSIGN_TIMEOUT_SECONDS=120
# ---------- in-toto attestation + SLSA provenance (v2.3-s2) ----------
# After the SBOM is signed (above), the worker also generates a SLSA provenance
# attestation (an in-toto Statement signed with `cosign attest-blob`) so a
# downstream consumer can verify HOW the SBOM was produced, not just that the
# bytes are intact. It reuses the SAME cosign key / keyless config as signing —
# no separate key. BEST-EFFORT: a missing cosign / unconfigured key / cosign
# failure logs a WARNING and the scan still succeeds (un-attested). The predicate
# carries ONLY the scan/project ids + build context (timestamps, builder
# id/version) — never secrets, the git URL, or workspace paths.
#
# builder.id — a URI naming THIS build platform in the provenance. The default is
# a vendor-neutral URN; override to name your own deployment so a verifier can
# pin provenance to a known builder (e.g. https://ci.example.com/trustedoss).
# Build-platform identity, NOT a secret. Read at call time (rule #11).
SLSA_BUILDER_ID=https://github.com/trustedoss/trusca/worker
# Version stamped into the provenance builder.version + SBOM generation context
# (satisfies the CISA-2025 / NTIA "tool name + version" element from the
# attestation alone). Defaults to the bundled portal version; set to the exact
# release tag in production. Read at call time (rule #11).
TRUSTEDOSS_VERSION=
# ---------- Jira (optional, stub by default) ----------
JIRA_ENABLED=false
JIRA_URL=
JIRA_TOKEN=