diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..ed5dd8b --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,26 @@ +name: Dependency diff review + +on: + pull_request: + branches: + - master + - work + +# Restrict to the minimum permissions needed for checkout and dependency review. +permissions: + contents: read + +jobs: + dependency-review: + name: Dependency diff review + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Dependency diff review + uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 + with: + fail-on-severity: high diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 0000000..8b97c9c --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,46 @@ +name: Trivy repository scan + +on: + push: + branches: + - master + - work + pull_request: + branches: + - master + - work + +# Restrict to minimum required permissions. +# security-events: write is required only for SARIF upload to code scanning. +permissions: + contents: read + security-events: write + +jobs: + trivy: + name: Trivy filesystem scan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Run Trivy filesystem scan + uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.30.0 + with: + scan-type: fs + scan-ref: "." + severity: HIGH,CRITICAL + ignore-unfixed: true + format: sarif + output: trivy-results.sarif + + - name: Upload Trivy SARIF to code scanning + # Skip on forked PRs — GitHub does not grant security-events: write to + # untrusted fork tokens, so SARIF upload would fail with a permissions error. + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + with: + sarif_file: trivy-results.sarif + category: trivy diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..8b6bf05 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,34 @@ +name: zizmor advisory audit + +on: + pull_request: + paths: + - ".github/workflows/**" + +# Restrict to minimum required permissions. +permissions: + contents: read + +jobs: + zizmor: + name: zizmor workflow audit + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Install zizmor + run: pip install zizmor==1.5.0 + + - name: Run zizmor workflow audit + # Advisory mode — findings are reported but do not fail the job. + # Maintainers should review and address findings before merging workflow changes. + run: | + EXIT_CODE=0 + zizmor --format plain .github/workflows/ || EXIT_CODE=$? + if [ $EXIT_CODE -ne 0 ]; then + echo "::warning::zizmor found workflow security findings (advisory). Review the output above before merging." + fi + exit 0 diff --git a/apps/api/src/env.ts b/apps/api/src/env.ts new file mode 100644 index 0000000..ef134a7 --- /dev/null +++ b/apps/api/src/env.ts @@ -0,0 +1,70 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +import dotenv from 'dotenv'; + +let envLoaded = false; + +export function loadRuntimeEnv(envPathCandidates?: string[]): void { + if (envLoaded) return; + + const candidates = + envPathCandidates ?? + [ + path.resolve(process.cwd(), '.env.local'), + path.resolve(process.cwd(), '.env'), + path.resolve(process.cwd(), '../../.env.local'), + path.resolve(process.cwd(), '../../.env') + ]; + + for (const envPath of candidates) { + dotenv.config({ path: envPath, override: false }); + } + + envLoaded = true; +} + +export function resolveDatabaseUrl(env: NodeJS.ProcessEnv = process.env): string | null { + const direct = (env.DATABASE_URL || '').trim(); + if (direct) return direct; + + const candidates = [env.SUPABASE_DB_URL, env.SUPABASE_POOLER_URL, env.SUPABASE_DIRECT_URL]; + + for (const candidate of candidates) { + const value = (candidate || '').trim(); + if (value) { + env.DATABASE_URL = value; + return value; + } + } + + const supabasePassword = (env.SUPABASE_DB_PASSWORD || '').trim(); + if (supabasePassword) { + const poolerCandidates = [ + path.resolve(process.cwd(), 'supabase/.temp/pooler-url'), + path.resolve(process.cwd(), '../../supabase/.temp/pooler-url'), + path.resolve(process.env.HOME || '', 'supabase/.temp/pooler-url') + ]; + + for (const poolerPath of poolerCandidates) { + try { + const rawPoolerUrl = readFileSync(poolerPath, 'utf-8').trim(); + if (!rawPoolerUrl) continue; + + const parsed = new URL(rawPoolerUrl); + if (!parsed.password) { + parsed.password = encodeURIComponent(supabasePassword); + } + parsed.searchParams.set('sslmode', 'require'); + + const resolved = parsed.toString(); + env.DATABASE_URL = resolved; + return resolved; + } catch { + // Continue searching candidate pooler URLs. + } + } + } + + return null; +}