diff --git a/.github/actions/pnpm-setup/action.yml b/.github/actions/pnpm-setup/action.yml
new file mode 100644
index 00000000..7de5ec9a
--- /dev/null
+++ b/.github/actions/pnpm-setup/action.yml
@@ -0,0 +1,49 @@
+name: Setup Node + pnpm
+description: >-
+ Composite action that installs pnpm, sets up Node with the pnpm store cached,
+ and runs `pnpm install --frozen-lockfile` at the repo root. Used across CI,
+ CD, and security workflows to keep setup logic in one place.
+
+inputs:
+ node-version:
+ description: Node.js version
+ required: false
+ default: "22"
+ install:
+ description: Whether to run `pnpm install --frozen-lockfile` after setup
+ required: false
+ default: "true"
+ ignore-scripts:
+ description: Pass --ignore-scripts to pnpm install (faster, skips postinstalls)
+ required: false
+ default: "false"
+
+runs:
+ using: composite
+ steps:
+ - name: Install pnpm
+ uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
+
+ - name: Setup Node
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: ${{ inputs.node-version }}
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ if: inputs.install == 'true'
+ shell: bash
+ # SECURITY: pass inputs through env rather than interpolating them
+ # into the shell body. Direct `${{ inputs.* }}` interpolation is a
+ # GHA command-injection vector even for composite actions (a caller
+ # workflow could pass a crafted value); env-var indirection is the
+ # standard mitigation.
+ env:
+ IGNORE_SCRIPTS: ${{ inputs.ignore-scripts }}
+ run: |
+ set -euo pipefail
+ if [ "${IGNORE_SCRIPTS:-false}" = "true" ]; then
+ pnpm install --frozen-lockfile --ignore-scripts
+ else
+ pnpm install --frozen-lockfile
+ fi
diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml
new file mode 100644
index 00000000..de83e82f
--- /dev/null
+++ b/.github/codeql/codeql-config.yml
@@ -0,0 +1,23 @@
+name: "Enclosed CodeQL config"
+
+# Use extended query suites for deeper coverage than the default
+queries:
+ - uses: security-extended
+ - uses: security-and-quality
+
+paths:
+ - packages
+
+paths-ignore:
+ - "**/node_modules"
+ - "**/dist"
+ - "**/dist-*"
+ - "**/.output"
+ - "**/.nuxt"
+ - "**/.nitro"
+ - "**/.wrangler"
+ - "**/coverage"
+ - "**/*.test.ts"
+ - "**/*.spec.ts"
+ - "**/*.e2e.test.ts"
+ - "packages/docs"
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..4453eb7b
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,104 @@
+version: 2
+updates:
+ # Keep GitHub Actions pinned and up-to-date.
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ labels:
+ - "dependencies"
+ - "github-actions"
+ commit-message:
+ prefix: "chore(deps)"
+ include: "scope"
+ groups:
+ actions-minor-patch:
+ update-types:
+ - "minor"
+ - "patch"
+
+ # Docker base images (node:22-slim / node:22-alpine).
+ - package-ecosystem: "docker"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 5
+ labels:
+ - "dependencies"
+ - "docker"
+ commit-message:
+ prefix: "chore(docker)"
+ include: "scope"
+
+ # npm security-only updates across every workspace package.
+ # Renovate handles day-to-day version bumps; Dependabot's role here is to
+ # raise CVE fixes quickly (daily cadence) via GitHub's native security
+ # advisory integration. open-pull-requests-limit: 0 disables version PRs,
+ # so only vulnerability-driven PRs are created, avoiding overlap with
+ # Renovate.
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 0
+ labels:
+ - "dependencies"
+ - "security"
+ commit-message:
+ prefix: "chore(deps)"
+ include: "scope"
+
+ - package-ecosystem: "npm"
+ directory: "/packages/app-client"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 0
+ labels:
+ - "dependencies"
+ - "security"
+
+ - package-ecosystem: "npm"
+ directory: "/packages/app-server"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 0
+ labels:
+ - "dependencies"
+ - "security"
+
+ - package-ecosystem: "npm"
+ directory: "/packages/cli"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 0
+ labels:
+ - "dependencies"
+ - "security"
+
+ - package-ecosystem: "npm"
+ directory: "/packages/crypto"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 0
+ labels:
+ - "dependencies"
+ - "security"
+
+ - package-ecosystem: "npm"
+ directory: "/packages/lib"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 0
+ labels:
+ - "dependencies"
+ - "security"
+
+ - package-ecosystem: "npm"
+ directory: "/packages/docs"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 0
+ labels:
+ - "dependencies"
+ - "security"
diff --git a/.github/workflows/cd-app-prod.yaml b/.github/workflows/cd-app-prod.yaml
index 79ee3427..212b7f33 100644
--- a/.github/workflows/cd-app-prod.yaml
+++ b/.github/workflows/cd-app-prod.yaml
@@ -5,6 +5,14 @@ on:
branches:
- main
+permissions:
+ contents: read
+
+concurrency:
+ # Never cancel production deploys mid-flight; serialize them instead.
+ group: cd-app-prod
+ cancel-in-progress: false
+
jobs:
publish-app-prod:
runs-on: ubuntu-latest
diff --git a/.github/workflows/cd-docker-release.yaml b/.github/workflows/cd-docker-release.yaml
index cf25462b..5923682d 100644
--- a/.github/workflows/cd-docker-release.yaml
+++ b/.github/workflows/cd-docker-release.yaml
@@ -35,6 +35,11 @@ permissions:
contents: read
packages: write
+concurrency:
+ # Serialize releases — never cancel a publish in-flight.
+ group: cd-docker-release-${{ github.ref }}
+ cancel-in-progress: false
+
jobs:
publish-crypto:
name: Publish @enclosed/crypto to npm
diff --git a/.github/workflows/cd-preview-build.yaml b/.github/workflows/cd-preview-build.yaml
index 97fa8f98..496ca74c 100644
--- a/.github/workflows/cd-preview-build.yaml
+++ b/.github/workflows/cd-preview-build.yaml
@@ -5,6 +5,13 @@ on:
pull_request:
types: [opened, synchronize]
+permissions:
+ contents: read
+
+concurrency:
+ group: cd-preview-build-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
jobs:
build-app-preview:
runs-on: ubuntu-latest
diff --git a/.github/workflows/cd-preview-deploy.yaml b/.github/workflows/cd-preview-deploy.yaml
index 6287526b..6675e1e1 100644
--- a/.github/workflows/cd-preview-deploy.yaml
+++ b/.github/workflows/cd-preview-deploy.yaml
@@ -12,6 +12,10 @@ permissions:
contents: read
pull-requests: write
+concurrency:
+ group: cd-preview-deploy-${{ github.event.workflow_run.head_branch }}
+ cancel-in-progress: true
+
jobs:
deploy-app-preview:
runs-on: ubuntu-latest
diff --git a/.github/workflows/ci-app-client.yaml b/.github/workflows/ci-app-client.yaml
index 4bb40568..c82b4be5 100644
--- a/.github/workflows/ci-app-client.yaml
+++ b/.github/workflows/ci-app-client.yaml
@@ -2,9 +2,26 @@ name: CI - App Client
on:
pull_request:
+ paths:
+ - 'packages/app-client/**'
+ - 'packages/lib/**'
+ - 'packages/crypto/**'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - 'package.json'
+ - '.github/workflows/ci-app-client.yaml'
+ - '.github/actions/pnpm-setup/**'
push:
branches:
- main
+ - twn-main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-app-client-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
ci-app-client:
@@ -16,18 +33,11 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-
- - uses: actions/setup-node@v4
with:
- node-version: 22
- cache: 'pnpm'
+ persist-credentials: false
- - name: Install dependencies
- run: pnpm i
- working-directory: ./
+ - name: Setup Node + pnpm
+ uses: ./.github/actions/pnpm-setup
- name: Run linters
run: pnpm lint
diff --git a/.github/workflows/ci-app-server.yaml b/.github/workflows/ci-app-server.yaml
index 31f3f00c..30c7fd32 100644
--- a/.github/workflows/ci-app-server.yaml
+++ b/.github/workflows/ci-app-server.yaml
@@ -2,9 +2,26 @@ name: CI - App Server
on:
pull_request:
+ paths:
+ - 'packages/app-server/**'
+ - 'packages/lib/**'
+ - 'packages/crypto/**'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - 'package.json'
+ - '.github/workflows/ci-app-server.yaml'
+ - '.github/actions/pnpm-setup/**'
push:
branches:
- main
+ - twn-main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-app-server-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
ci-app-server:
@@ -16,18 +33,11 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-
- - uses: actions/setup-node@v4
with:
- node-version: 22
- cache: 'pnpm'
+ persist-credentials: false
- - name: Install dependencies
- run: pnpm i
- working-directory: ./
+ - name: Setup Node + pnpm
+ uses: ./.github/actions/pnpm-setup
- name: Run linters
run: pnpm lint
diff --git a/.github/workflows/ci-cli.yaml b/.github/workflows/ci-cli.yaml
index 3e2e4855..6713f89b 100644
--- a/.github/workflows/ci-cli.yaml
+++ b/.github/workflows/ci-cli.yaml
@@ -2,9 +2,26 @@ name: CI - Cli
on:
pull_request:
+ paths:
+ - 'packages/cli/**'
+ - 'packages/lib/**'
+ - 'packages/crypto/**'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - 'package.json'
+ - '.github/workflows/ci-cli.yaml'
+ - '.github/actions/pnpm-setup/**'
push:
branches:
- main
+ - twn-main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-cli-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
ci-cli:
@@ -16,18 +33,11 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-
- - uses: actions/setup-node@v4
with:
- node-version: 22
- cache: 'pnpm'
+ persist-credentials: false
- - name: Install dependencies
- run: pnpm i
- working-directory: ./
+ - name: Setup Node + pnpm
+ uses: ./.github/actions/pnpm-setup
- name: Run linters
run: pnpm lint
diff --git a/.github/workflows/ci-crypto.yaml b/.github/workflows/ci-crypto.yaml
index 082344ee..298d6e37 100644
--- a/.github/workflows/ci-crypto.yaml
+++ b/.github/workflows/ci-crypto.yaml
@@ -2,9 +2,24 @@ name: CI - Crypto
on:
pull_request:
+ paths:
+ - 'packages/crypto/**'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - 'package.json'
+ - '.github/workflows/ci-crypto.yaml'
+ - '.github/actions/pnpm-setup/**'
push:
branches:
- main
+ - twn-main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-crypto-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
ci-lib:
@@ -16,18 +31,11 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-
- - uses: actions/setup-node@v4
with:
- node-version: 22
- cache: 'pnpm'
+ persist-credentials: false
- - name: Install dependencies
- run: pnpm i
- working-directory: ./
+ - name: Setup Node + pnpm
+ uses: ./.github/actions/pnpm-setup
- name: Run linters
run: pnpm lint
diff --git a/.github/workflows/ci-deploy-cloudflare.yaml b/.github/workflows/ci-deploy-cloudflare.yaml
index 21ccff3a..fb74325c 100644
--- a/.github/workflows/ci-deploy-cloudflare.yaml
+++ b/.github/workflows/ci-deploy-cloudflare.yaml
@@ -2,9 +2,24 @@ name: CI - Deploy Cloudflare
on:
pull_request:
+ paths:
+ - 'packages/**'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - 'package.json'
+ - '.github/workflows/ci-deploy-cloudflare.yaml'
+ - '.github/actions/pnpm-setup/**'
push:
branches:
- main
+ - twn-main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-deploy-cloudflare-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
ci-deploy-cloudflare:
@@ -16,18 +31,11 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-
- - uses: actions/setup-node@v4
with:
- node-version: 22
- cache: 'pnpm'
+ persist-credentials: false
- - name: Install dependencies
- run: pnpm i
- working-directory: ./
+ - name: Setup Node + pnpm
+ uses: ./.github/actions/pnpm-setup
- name: Build the app
run: pnpm build
diff --git a/.github/workflows/ci-docs.yaml b/.github/workflows/ci-docs.yaml
index 7c170fc2..974dc817 100644
--- a/.github/workflows/ci-docs.yaml
+++ b/.github/workflows/ci-docs.yaml
@@ -2,9 +2,24 @@ name: CI - Docs
on:
pull_request:
+ paths:
+ - 'packages/docs/**'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - 'package.json'
+ - '.github/workflows/ci-docs.yaml'
+ - '.github/actions/pnpm-setup/**'
push:
branches:
- main
+ - twn-main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-docs-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
ci-docs:
@@ -16,18 +31,11 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-
- - uses: actions/setup-node@v4
with:
- node-version: 22
- cache: 'pnpm'
+ persist-credentials: false
- - name: Install dependencies
- run: pnpm i
- working-directory: ./
+ - name: Setup Node + pnpm
+ uses: ./.github/actions/pnpm-setup
- name: Run linters
run: pnpm lint
diff --git a/.github/workflows/ci-lib.yaml b/.github/workflows/ci-lib.yaml
index 7d11103a..87cddd6f 100644
--- a/.github/workflows/ci-lib.yaml
+++ b/.github/workflows/ci-lib.yaml
@@ -2,9 +2,25 @@ name: CI - Lib
on:
pull_request:
+ paths:
+ - 'packages/lib/**'
+ - 'packages/crypto/**'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - 'package.json'
+ - '.github/workflows/ci-lib.yaml'
+ - '.github/actions/pnpm-setup/**'
push:
branches:
- main
+ - twn-main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-lib-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
ci-lib:
@@ -16,18 +32,11 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-
- - uses: actions/setup-node@v4
with:
- node-version: 22
- cache: 'pnpm'
+ persist-credentials: false
- - name: Install dependencies
- run: pnpm i
- working-directory: ./
+ - name: Setup Node + pnpm
+ uses: ./.github/actions/pnpm-setup
- name: Run linters
run: pnpm lint
diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml
index 70698676..2621ab9a 100644
--- a/.github/workflows/ci-test-e2e.yml
+++ b/.github/workflows/ci-test-e2e.yml
@@ -2,9 +2,27 @@ name: CI - App Client E2E
on:
pull_request:
+ paths:
+ - 'packages/app-client/**'
+ - 'packages/app-server/**'
+ - 'packages/lib/**'
+ - 'packages/crypto/**'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - 'package.json'
+ - '.github/workflows/ci-test-e2e.yml'
+ - '.github/actions/pnpm-setup/**'
push:
branches:
- main
+ - twn-main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-e2e-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
run-e2es:
@@ -16,23 +34,16 @@ jobs:
working-directory: packages/app-client
steps:
- - uses: actions/checkout@v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-
- - uses: actions/setup-node@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
- node-version: 22
- cache: 'pnpm'
+ persist-credentials: false
+
+ - name: Setup Node + pnpm
+ uses: ./.github/actions/pnpm-setup
- name: Get Playwright version
id: playwright-version
run: echo "PLAYWRIGHT_VERSION=$(pnpm list --json | jq -r .[0].devDependencies.\"@playwright/test\".version)" >> "$GITHUB_OUTPUT"
-
- - name: Install dependencies
- run: pnpm i
- working-directory: ./
- name: Build iso-prod app
run: |
diff --git a/.github/workflows/code-coverage.yaml b/.github/workflows/code-coverage.yaml
new file mode 100644
index 00000000..58ce798a
--- /dev/null
+++ b/.github/workflows/code-coverage.yaml
@@ -0,0 +1,81 @@
+name: CI - Code Coverage
+
+on:
+ pull_request:
+ branches: [main, twn-main]
+ push:
+ branches: [main, twn-main]
+
+permissions:
+ contents: read
+
+concurrency:
+ group: code-coverage-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ coverage:
+ name: Coverage (${{ matrix.pkg.name }})
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ permissions:
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ # Packages with @vitest/coverage-v8 installed. app-client lacks the
+ # coverage provider (Playwright is used for e2e there) so is excluded.
+ # `slug` must be filesystem-safe for use in artifact names - no `@`
+ # or `/` chars, which actions/upload-artifact@v4 rejects.
+ pkg:
+ - { name: "@enclosed/crypto", slug: "crypto", dir: "packages/crypto" }
+ - { name: "@enclosed/lib", slug: "lib", dir: "packages/lib" }
+ - { name: "@enclosed/app-server", slug: "app-server", dir: "packages/app-server" }
+ - { name: "@enclosed/cli", slug: "cli", dir: "packages/cli" }
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: Setup Node + pnpm
+ uses: ./.github/actions/pnpm-setup
+
+ - name: Run tests with coverage
+ continue-on-error: true
+ working-directory: ${{ matrix.pkg.dir }}
+ run: |
+ pnpm exec vitest run \
+ --coverage \
+ --coverage.reporter=text \
+ --coverage.reporter=lcov \
+ --coverage.reporter=json-summary
+
+ - name: Upload coverage artifact
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-${{ matrix.pkg.slug }}
+ path: ${{ matrix.pkg.dir }}/coverage
+ if-no-files-found: ignore
+ retention-days: 14
+
+ - name: Upload coverage to Codecov
+ # Soft-fail: a missing CODECOV_TOKEN or a transient Codecov outage
+ # should not break CI - the coverage artifact above is always
+ # uploaded regardless.
+ if: always() && hashFiles(format('{0}/coverage/lcov.info', matrix.pkg.dir)) != ''
+ continue-on-error: true
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: ${{ matrix.pkg.dir }}/coverage/lcov.info
+ # Codecov flags must match ^[\w\.\-]+$ - use the slug, not the
+ # @scoped package name.
+ flags: ${{ matrix.pkg.slug }}
+ name: ${{ matrix.pkg.slug }}
+ fail_ci_if_error: false
+ verbose: true
diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml
new file mode 100644
index 00000000..b3d28ad5
--- /dev/null
+++ b/.github/workflows/codeql.yaml
@@ -0,0 +1,48 @@
+name: Security - CodeQL
+
+on:
+ push:
+ branches: [main, twn-main]
+ pull_request:
+ branches: [main, twn-main]
+ schedule:
+ # Weekly re-scan so advisories published after a merge still surface
+ - cron: '17 4 * * 1'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: codeql-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ analyze:
+ name: CodeQL (${{ matrix.language }})
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ permissions:
+ contents: read
+ security-events: write
+ actions: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [javascript-typescript]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ config-file: ./.github/codeql/codeql-config.yml
+ queries: security-extended,security-and-quality
+
+ - name: Perform CodeQL analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{ matrix.language }}"
diff --git a/.github/workflows/dast-zap.yaml b/.github/workflows/dast-zap.yaml
new file mode 100644
index 00000000..e5d044f1
--- /dev/null
+++ b/.github/workflows/dast-zap.yaml
@@ -0,0 +1,129 @@
+name: Security - DAST (OWASP ZAP)
+
+on:
+ schedule:
+ # Nightly against the running app built from main
+ - cron: '27 3 * * *'
+ workflow_dispatch:
+ inputs:
+ target_url:
+ description: 'Override target URL (defaults to local ephemeral container)'
+ required: false
+ default: ''
+
+permissions:
+ contents: read
+
+concurrency:
+ group: dast-zap
+ cancel-in-progress: false
+
+jobs:
+ zap-baseline:
+ name: OWASP ZAP baseline scan
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ permissions:
+ contents: read
+ issues: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build enclosed image
+ if: ${{ github.event.inputs.target_url == '' }}
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./Dockerfile
+ push: false
+ load: true
+ tags: enclosed:dast
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Start enclosed container
+ if: ${{ github.event.inputs.target_url == '' }}
+ run: |
+ docker run -d --name enclosed-dast -p 8787:8787 enclosed:dast
+ # Wait for readiness (up to 60s)
+ for i in $(seq 1 30); do
+ if curl -fsS http://localhost:8787 >/dev/null 2>&1; then
+ echo "enclosed is up"
+ exit 0
+ fi
+ sleep 2
+ done
+ echo "enclosed failed to start" >&2
+ docker logs enclosed-dast || true
+ exit 1
+
+ - name: Resolve target URL
+ id: target
+ # SECURITY: pass the workflow_dispatch input through an env var rather
+ # than interpolating it directly into the shell script body. Direct
+ # `${{ github.event.inputs.* }}` interpolation in `run:` is a
+ # classic GHA command-injection vector (a user with write access
+ # could execute arbitrary shell on the runner by passing a
+ # malicious URL). Also validate the override strictly to prevent
+ # the ZAP scanner from becoming an SSRF primitive against cloud
+ # metadata endpoints or the runner's local network.
+ env:
+ OVERRIDE_URL: ${{ github.event.inputs.target_url }}
+ run: |
+ set -euo pipefail
+ if [ -z "${OVERRIDE_URL:-}" ]; then
+ printf 'url=%s\n' 'http://localhost:8787' >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ # 1. Shape check - require plain http(s) URL, no backticks/$/;/\|/&/ /'/"/\r/\n.
+ if ! printf '%s' "$OVERRIDE_URL" | grep -Eq '^https?://[A-Za-z0-9._-]+(:[0-9]{1,5})?(/[A-Za-z0-9._~:/?#@!$&()*+,;=%[-]*)?$'; then
+ echo "::error::Rejected target_url: does not match allowed URL shape." >&2
+ exit 1
+ fi
+ # 2. Host allowlist - deny cloud metadata, link-local, RFC1918 leakage
+ # (except explicit localhost), and unqualified hostnames.
+ host=$(printf '%s' "$OVERRIDE_URL" | sed -E 's#^https?://([^/:]+).*#\1#')
+ case "$host" in
+ 169.254.*|metadata.google.internal|metadata.azure.com|metadata.aws.amazon.com|100.100.100.200)
+ echo "::error::Rejected target_url: cloud metadata / link-local host." >&2
+ exit 1
+ ;;
+ 10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.2[0-9].*|172.3[0-1].*|192.168.*)
+ echo "::error::Rejected target_url: RFC1918 private IP." >&2
+ exit 1
+ ;;
+ localhost|127.0.0.1|127.*)
+ # Loopback explicitly allowed - the default case is localhost:8787.
+ ;;
+ *.*)
+ # Public FQDN - allowed.
+ ;;
+ *)
+ echo "::error::Rejected target_url: unqualified host." >&2
+ exit 1
+ ;;
+ esac
+ printf 'url=%s\n' "$OVERRIDE_URL" >> "$GITHUB_OUTPUT"
+
+ - name: OWASP ZAP baseline scan
+ uses: zaproxy/action-baseline@7c4deb10e6261301961c86d65d54a516394f9aed # v0.14.0
+ with:
+ target: ${{ steps.target.outputs.url }}
+ rules_file_name: .zap/rules.tsv
+ cmd_options: '-a -j -T 10'
+ fail_action: false
+ allow_issue_writing: true
+
+ - name: Stop enclosed container
+ if: always() && github.event.inputs.target_url == ''
+ run: |
+ set -eu
+ docker logs enclosed-dast || true
+ docker rm -f enclosed-dast || true
diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml
new file mode 100644
index 00000000..ad521180
--- /dev/null
+++ b/.github/workflows/dependency-review.yaml
@@ -0,0 +1,40 @@
+name: Security - Dependency Review
+
+on:
+ pull_request:
+ branches: [main, twn-main]
+
+permissions:
+ contents: read
+
+concurrency:
+ group: dependency-review-${{ github.workflow }}-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ dependency-review:
+ name: Dependency Review
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ permissions:
+ contents: read
+ pull-requests: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ # dependency-review-action requires GitHub Advanced Security on private
+ # repos. Allow the step to soft-fail so the PR isn't blocked on
+ # infrastructure availability; findings (when accessible) are still
+ # reported as a PR comment and fail the check, but merge remains
+ # possible.
+ - name: Dependency Review
+ uses: actions/dependency-review-action@v4
+ continue-on-error: true
+ with:
+ fail-on-severity: high
+ comment-summary-in-pr: on-failure
+ deny-licenses: AGPL-1.0-only,AGPL-1.0-or-later,AGPL-3.0-only,AGPL-3.0-or-later,GPL-2.0-only,GPL-2.0-or-later,GPL-3.0-only,GPL-3.0-or-later,LGPL-2.0-only,LGPL-2.0-or-later,LGPL-2.1-only,LGPL-2.1-or-later,LGPL-3.0-only,LGPL-3.0-or-later
diff --git a/.github/workflows/grype.yaml b/.github/workflows/grype.yaml
new file mode 100644
index 00000000..5ccd679a
--- /dev/null
+++ b/.github/workflows/grype.yaml
@@ -0,0 +1,143 @@
+name: Security - Grype
+
+on:
+ push:
+ branches: [main, twn-main]
+ pull_request:
+ branches: [main, twn-main]
+ schedule:
+ - cron: '47 6 * * *'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: grype-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ grype-fs:
+ name: Grype - Filesystem
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+ security-events: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: Cache Grype vuln DB
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/grype/db
+ key: grype-db-${{ runner.os }}-${{ github.run_id }}
+ restore-keys: |
+ grype-db-${{ runner.os }}-
+
+ - name: Grype scan (source tree)
+ id: grype
+ uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 # v6.0.0
+ continue-on-error: true
+ with:
+ path: '.'
+ # Informational for first-run baseline; flip to true to enforce.
+ fail-build: false
+ severity-cutoff: high
+ output-format: sarif
+ only-fixed: true
+
+ - name: Upload Grype FS SARIF
+ if: always() && github.event_name != 'pull_request' && steps.grype.outputs.sarif != ''
+ continue-on-error: true
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: ${{ steps.grype.outputs.sarif }}
+ category: grype-fs
+
+ - name: Upload Grype FS SARIF artifact (PR)
+ if: always() && github.event_name == 'pull_request' && steps.grype.outputs.sarif != ''
+ continue-on-error: true
+ uses: actions/upload-artifact@v4
+ with:
+ name: grype-fs-sarif
+ path: ${{ steps.grype.outputs.sarif }}
+ retention-days: 14
+
+ grype-image:
+ name: Grype - Container image
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ permissions:
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ dockerfile:
+ - { file: Dockerfile, tag: enclosed:scan }
+ - { file: Dockerfile.rootless, tag: enclosed:scan-rootless }
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: Cache Grype vuln DB
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/grype/db
+ key: grype-db-${{ runner.os }}-${{ github.run_id }}
+ restore-keys: |
+ grype-db-${{ runner.os }}-
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build image (local, no push)
+ id: build
+ uses: docker/build-push-action@v6
+ continue-on-error: true
+ with:
+ context: .
+ file: ./${{ matrix.dockerfile.file }}
+ push: false
+ load: true
+ platforms: linux/amd64
+ tags: ${{ matrix.dockerfile.tag }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Grype scan (image)
+ id: grype-image
+ uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 # v6.0.0
+ continue-on-error: true
+ with:
+ image: ${{ matrix.dockerfile.tag }}
+ fail-build: false
+ severity-cutoff: high
+ output-format: sarif
+ only-fixed: true
+
+ - name: Upload Grype image SARIF
+ if: always() && github.event_name != 'pull_request' && steps.grype-image.outputs.sarif != ''
+ continue-on-error: true
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: ${{ steps.grype-image.outputs.sarif }}
+ category: grype-image-${{ matrix.dockerfile.file }}
+
+ - name: Upload Grype image SARIF artifact (PR)
+ if: always() && github.event_name == 'pull_request' && steps.grype-image.outputs.sarif != ''
+ continue-on-error: true
+ uses: actions/upload-artifact@v4
+ with:
+ name: grype-image-${{ matrix.dockerfile.file }}-sarif
+ path: ${{ steps.grype-image.outputs.sarif }}
+ retention-days: 14
diff --git a/.github/workflows/osv-scanner.yaml b/.github/workflows/osv-scanner.yaml
new file mode 100644
index 00000000..635670f3
--- /dev/null
+++ b/.github/workflows/osv-scanner.yaml
@@ -0,0 +1,99 @@
+name: Security - OSV-Scanner & pnpm audit
+
+on:
+ push:
+ branches: [main, twn-main]
+ pull_request:
+ branches: [main, twn-main]
+ schedule:
+ - cron: '13 8 * * *'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: osv-scanner-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ osv-scanner:
+ name: OSV-Scanner
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+ security-events: write
+ actions: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: OSV-Scanner
+ uses: google/osv-scanner-action/osv-scanner-action@c51854704019a247608d928f370c98740469d4b5 # v2.3.5
+ with:
+ scan-args: |-
+ --lockfile=./pnpm-lock.yaml
+ --format=sarif
+ --output=osv-scanner.sarif
+ --recursive
+ continue-on-error: true
+
+ - name: Upload OSV SARIF
+ # Only on push/schedule; PRs would fail Code Scanning delta against
+ # an empty main baseline. The SARIF is still kept as an artifact
+ # below.
+ if: always() && github.event_name != 'pull_request' && hashFiles('osv-scanner.sarif') != ''
+ continue-on-error: true
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: osv-scanner.sarif
+ category: osv-scanner
+
+ - name: Upload OSV SARIF artifact
+ if: always() && hashFiles('osv-scanner.sarif') != ''
+ uses: actions/upload-artifact@v4
+ with:
+ name: osv-scanner-sarif
+ path: osv-scanner.sarif
+ retention-days: 30
+
+ pnpm-audit:
+ name: pnpm audit (high & critical)
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: Setup Node + pnpm
+ uses: ./.github/actions/pnpm-setup
+ with:
+ ignore-scripts: "true"
+
+ - name: pnpm audit (surface HIGH/CRITICAL in prod deps)
+ # Informational first-run: surface findings without blocking merges so
+ # the baseline can be established, then flip `continue-on-error` to
+ # false once the 0-CVE target is achieved.
+ continue-on-error: true
+ run: pnpm audit --audit-level high --prod
+
+ - name: pnpm audit (JSON report, all severities)
+ if: always()
+ run: pnpm audit --json > pnpm-audit.json || true
+
+ - name: Upload audit report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: pnpm-audit-report
+ path: pnpm-audit.json
+ retention-days: 30
diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml
new file mode 100644
index 00000000..f6875bad
--- /dev/null
+++ b/.github/workflows/sbom.yaml
@@ -0,0 +1,129 @@
+name: Security - SBOM
+
+on:
+ push:
+ branches: [main, twn-main]
+ tags:
+ - 'v*.*.*'
+ pull_request:
+ branches: [main, twn-main]
+ schedule:
+ - cron: '11 7 * * 1'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: sbom-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ sbom-source:
+ name: SBOM - Source tree
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: Setup Node + pnpm
+ uses: ./.github/actions/pnpm-setup
+ with:
+ ignore-scripts: "true"
+
+ - name: Generate CycloneDX SBOM (Anchore)
+ uses: anchore/sbom-action@62ad5284b8ced813296287a0b63906cb364b73ee # v0.22.0
+ with:
+ path: .
+ format: cyclonedx-json
+ output-file: sbom.source.cyclonedx.json
+ artifact-name: sbom-source-cyclonedx
+
+ - name: Generate SPDX SBOM (Anchore)
+ uses: anchore/sbom-action@62ad5284b8ced813296287a0b63906cb364b73ee # v0.22.0
+ with:
+ path: .
+ format: spdx-json
+ output-file: sbom.source.spdx.json
+ artifact-name: sbom-source-spdx
+
+ - name: Upload SBOM artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: sbom-source
+ path: |
+ sbom.source.cyclonedx.json
+ sbom.source.spdx.json
+ retention-days: 90
+
+ sbom-image:
+ name: SBOM - Container image
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ permissions:
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ dockerfile:
+ - { file: Dockerfile, tag: enclosed:sbom, slug: root }
+ - { file: Dockerfile.rootless, tag: enclosed:sbom-rootless, slug: rootless }
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build image (local, no push)
+ id: build
+ uses: docker/build-push-action@v6
+ continue-on-error: true
+ with:
+ context: .
+ file: ./${{ matrix.dockerfile.file }}
+ push: false
+ load: true
+ platforms: linux/amd64
+ tags: ${{ matrix.dockerfile.tag }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Generate CycloneDX SBOM for image
+ if: steps.build.outcome == 'success'
+ uses: anchore/sbom-action@62ad5284b8ced813296287a0b63906cb364b73ee # v0.22.0
+ with:
+ image: ${{ matrix.dockerfile.tag }}
+ format: cyclonedx-json
+ output-file: sbom.image.${{ matrix.dockerfile.slug }}.cyclonedx.json
+ artifact-name: sbom-image-${{ matrix.dockerfile.slug }}-cyclonedx
+
+ - name: Generate SPDX SBOM for image
+ if: steps.build.outcome == 'success'
+ uses: anchore/sbom-action@62ad5284b8ced813296287a0b63906cb364b73ee # v0.22.0
+ with:
+ image: ${{ matrix.dockerfile.tag }}
+ format: spdx-json
+ output-file: sbom.image.${{ matrix.dockerfile.slug }}.spdx.json
+ artifact-name: sbom-image-${{ matrix.dockerfile.slug }}-spdx
+
+ - name: Upload SBOM artifacts
+ if: steps.build.outcome == 'success'
+ uses: actions/upload-artifact@v4
+ with:
+ name: sbom-image-${{ matrix.dockerfile.slug }}
+ path: |
+ sbom.image.${{ matrix.dockerfile.slug }}.cyclonedx.json
+ sbom.image.${{ matrix.dockerfile.slug }}.spdx.json
+ retention-days: 90
+ if-no-files-found: ignore
diff --git a/.github/workflows/semgrep.yaml b/.github/workflows/semgrep.yaml
new file mode 100644
index 00000000..6ae2e2bf
--- /dev/null
+++ b/.github/workflows/semgrep.yaml
@@ -0,0 +1,70 @@
+name: Security - Semgrep
+
+on:
+ push:
+ branches: [main, twn-main]
+ pull_request:
+ branches: [main, twn-main]
+ schedule:
+ - cron: '23 5 * * 1'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: semgrep-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ semgrep:
+ name: Semgrep SAST
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ permissions:
+ contents: read
+ security-events: write
+ container:
+ image: semgrep/semgrep:latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+
+ - name: Semgrep scan (SARIF)
+ # Curated rule packs covering OWASP Top 10, JS/TS, Node, and secrets.
+ # SEMGREP_APP_TOKEN is optional; when present, findings are uploaded to
+ # Semgrep Cloud for triage.
+ env:
+ SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
+ run: |
+ semgrep ci \
+ --sarif --output=semgrep.sarif \
+ --config "p/ci" \
+ --config "p/security-audit" \
+ --config "p/owasp-top-ten" \
+ --config "p/javascript" \
+ --config "p/typescript" \
+ --config "p/nodejs" \
+ --config "p/secrets" \
+ --config "p/dockerfile" \
+ --config "p/github-actions" \
+ --metrics=off \
+ || true
+
+ - name: Upload SARIF to GitHub code scanning
+ # Only on push/schedule - PRs would fail the Code Scanning delta check
+ # against an empty main baseline. See trivy.yaml for rationale.
+ if: always() && github.event_name != 'pull_request' && hashFiles('semgrep.sarif') != ''
+ continue-on-error: true
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: semgrep.sarif
+ category: semgrep
+
+ - name: Upload Semgrep SARIF artifact
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: semgrep-sarif
+ path: semgrep.sarif
+ retention-days: 30
diff --git a/.github/workflows/stale-action.yaml b/.github/workflows/stale-action.yaml
index dc7cfcc5..b770d63d 100644
--- a/.github/workflows/stale-action.yaml
+++ b/.github/workflows/stale-action.yaml
@@ -2,15 +2,23 @@ name: 'Close stale issues and PRs'
on:
schedule:
- cron: '42 2 * * *'
-
+
+permissions:
+ issues: write
+ pull-requests: write
+
+concurrency:
+ group: stale-action
+ cancel-in-progress: false
+
jobs:
stale:
name: Close stale issues and PRs
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v9
+ - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with:
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
days-before-stale: 30
- days-before-close: 5
\ No newline at end of file
+ days-before-close: 5
diff --git a/.github/workflows/trivy.yaml b/.github/workflows/trivy.yaml
new file mode 100644
index 00000000..1bb4c25c
--- /dev/null
+++ b/.github/workflows/trivy.yaml
@@ -0,0 +1,268 @@
+name: Security - Trivy
+
+on:
+ push:
+ branches: [main, twn-main]
+ pull_request:
+ branches: [main, twn-main]
+ schedule:
+ - cron: '37 6 * * *'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: trivy-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ trivy-fs:
+ name: Trivy - Filesystem (deps & secrets)
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ permissions:
+ contents: read
+ security-events: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: Cache Trivy vuln DB
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/trivy
+ key: trivy-db-${{ runner.os }}-${{ github.run_id }}
+ restore-keys: |
+ trivy-db-${{ runner.os }}-
+
+ - name: Trivy FS scan (SARIF, all findings)
+ uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
+ continue-on-error: true
+ with:
+ scan-type: fs
+ scan-ref: .
+ scanners: vuln,secret,misconfig,license
+ severity: CRITICAL,HIGH,MEDIUM
+ ignore-unfixed: true
+ format: sarif
+ output: trivy-fs.sarif
+ exit-code: '0'
+
+ - name: Upload Trivy FS SARIF
+ # Only upload on push/schedule so the first merge to main establishes
+ # the Code Scanning baseline. PRs get findings via job logs and an
+ # artifact, but do not trigger a delta comparison that would fail
+ # because the base branch has no prior baseline.
+ if: always() && github.event_name != 'pull_request' && hashFiles('trivy-fs.sarif') != ''
+ continue-on-error: true
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: trivy-fs.sarif
+ category: trivy-fs
+
+ - name: Upload Trivy FS SARIF artifact (PR)
+ if: always() && github.event_name == 'pull_request' && hashFiles('trivy-fs.sarif') != ''
+ continue-on-error: true
+ uses: actions/upload-artifact@v4
+ with:
+ name: trivy-fs-sarif
+ path: trivy-fs.sarif
+ retention-days: 14
+
+ - name: Trivy FS scan (table, surface HIGH/CRITICAL fixable)
+ # informational until baseline is green; flip exit-code to '1' to enforce.
+ uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
+ continue-on-error: true
+ with:
+ scan-type: fs
+ scan-ref: .
+ scanners: vuln,secret
+ severity: CRITICAL,HIGH
+ ignore-unfixed: true
+ format: table
+ exit-code: '0'
+
+ trivy-config:
+ name: Trivy - IaC / Dockerfile misconfig
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+ security-events: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: Cache Trivy vuln DB
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/trivy
+ key: trivy-db-${{ runner.os }}-${{ github.run_id }}
+ restore-keys: |
+ trivy-db-${{ runner.os }}-
+
+ - name: Trivy config scan (SARIF)
+ uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
+ continue-on-error: true
+ with:
+ scan-type: config
+ scan-ref: .
+ severity: CRITICAL,HIGH,MEDIUM
+ format: sarif
+ output: trivy-config.sarif
+ exit-code: '0'
+
+ - name: Upload Trivy config SARIF
+ if: always() && github.event_name != 'pull_request' && hashFiles('trivy-config.sarif') != ''
+ continue-on-error: true
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: trivy-config.sarif
+ category: trivy-config
+
+ - name: Upload Trivy config SARIF artifact (PR)
+ if: always() && github.event_name == 'pull_request' && hashFiles('trivy-config.sarif') != ''
+ continue-on-error: true
+ uses: actions/upload-artifact@v4
+ with:
+ name: trivy-config-sarif
+ path: trivy-config.sarif
+ retention-days: 14
+
+ trivy-image:
+ name: Trivy - Container image
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ permissions:
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ dockerfile:
+ - { file: Dockerfile, tag: enclosed:scan }
+ - { file: Dockerfile.rootless, tag: enclosed:scan-rootless }
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: Cache Trivy vuln DB
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/trivy
+ key: trivy-db-${{ runner.os }}-${{ github.run_id }}
+ restore-keys: |
+ trivy-db-${{ runner.os }}-
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build image (local, no push)
+ id: build
+ uses: docker/build-push-action@v6
+ continue-on-error: true
+ with:
+ context: .
+ file: ./${{ matrix.dockerfile.file }}
+ push: false
+ load: true
+ platforms: linux/amd64
+ tags: ${{ matrix.dockerfile.tag }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Trivy image scan (SARIF)
+ uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
+ continue-on-error: true
+ with:
+ image-ref: ${{ matrix.dockerfile.tag }}
+ scanners: vuln,secret
+ severity: CRITICAL,HIGH,MEDIUM
+ ignore-unfixed: true
+ format: sarif
+ output: trivy-image-${{ matrix.dockerfile.file }}.sarif
+ exit-code: '0'
+
+ - name: Upload Trivy image SARIF
+ if: always() && github.event_name != 'pull_request' && hashFiles(format('trivy-image-{0}.sarif', matrix.dockerfile.file)) != ''
+ continue-on-error: true
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: trivy-image-${{ matrix.dockerfile.file }}.sarif
+ category: trivy-image-${{ matrix.dockerfile.file }}
+
+ - name: Upload Trivy image SARIF artifact (PR)
+ if: always() && github.event_name == 'pull_request' && hashFiles(format('trivy-image-{0}.sarif', matrix.dockerfile.file)) != ''
+ continue-on-error: true
+ uses: actions/upload-artifact@v4
+ with:
+ name: trivy-image-${{ matrix.dockerfile.file }}-sarif
+ path: trivy-image-${{ matrix.dockerfile.file }}.sarif
+ retention-days: 14
+
+ - name: Trivy image scan (table, surface HIGH/CRITICAL fixable)
+ # informational until baseline is green; flip exit-code to '1' to enforce.
+ uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
+ continue-on-error: true
+ with:
+ image-ref: ${{ matrix.dockerfile.tag }}
+ scanners: vuln
+ severity: CRITICAL,HIGH
+ ignore-unfixed: true
+ format: table
+ exit-code: '0'
+
+ hadolint:
+ name: Hadolint - Dockerfile lint
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ dockerfile: [Dockerfile, Dockerfile.rootless]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ persist-credentials: false
+
+ - name: Hadolint
+ uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0
+ with:
+ dockerfile: ${{ matrix.dockerfile }}
+ format: sarif
+ output-file: hadolint-${{ matrix.dockerfile }}.sarif
+ no-fail: true
+
+ - name: Upload Hadolint SARIF
+ if: always() && github.event_name != 'pull_request' && hashFiles(format('hadolint-{0}.sarif', matrix.dockerfile)) != ''
+ continue-on-error: true
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: hadolint-${{ matrix.dockerfile }}.sarif
+ category: hadolint-${{ matrix.dockerfile }}
+
+ - name: Upload Hadolint SARIF artifact (PR)
+ if: always() && github.event_name == 'pull_request' && hashFiles(format('hadolint-{0}.sarif', matrix.dockerfile)) != ''
+ continue-on-error: true
+ uses: actions/upload-artifact@v4
+ with:
+ name: hadolint-${{ matrix.dockerfile }}-sarif
+ path: hadolint-${{ matrix.dockerfile }}.sarif
+ retention-days: 14
diff --git a/.semgrepignore b/.semgrepignore
new file mode 100644
index 00000000..e1b7263f
--- /dev/null
+++ b/.semgrepignore
@@ -0,0 +1,20 @@
+node_modules/
+dist/
+dist-*/
+.output/
+.nuxt/
+.nitro/
+.wrangler/
+coverage/
+cache/
+*.min.js
+*.min.mjs
+*.min.cjs
+pnpm-lock.yaml
+packages/docs/
+packages/deploy-cloudflare/dist/
+**/*.test.ts
+**/*.spec.ts
+**/*.e2e.test.ts
+playwright-report/
+test-results/
diff --git a/.zap/rules.tsv b/.zap/rules.tsv
new file mode 100644
index 00000000..f532a63f
--- /dev/null
+++ b/.zap/rules.tsv
@@ -0,0 +1,14 @@
+# ZAP baseline rule tuning for Enclosed.
+# Format:
- Learn more about our security architecture on our{' '}
+ Learn more about our security architecture on our
+ {' '}
Privacy & Security
- {' '}page.
+ {' '}
+ page.
- This instance is hosted and maintained by{' '}
+ This instance is hosted and maintained by
+ {' '}
TWN Systems
@@ -137,22 +140,26 @@ export const AboutPage: Component = () => {
- This service is based on{' '}
+ This service is based on
+ {' '}
Enclosed
- , an open-source project created by{' '}
+ , an open-source project created by
+ {' '}
Corentin Thomasset
. We're grateful for his work in creating a tool that prioritizes user privacy and security.
- Our fork is available at{' '}
+ Our fork is available at
+ {' '}
github.com/TWN-Systems/enclosed
- {' '}where you can:
+ {' '}
+ where you can:
- For more details about our analytics practices, please see our{' '}
+ For more details about our analytics practices, please see our
+ {' '}
Privacy & Security
- {' '}page.
+ {' '}
+ page.
- For questions about these terms, please visit our{' '}
+ For questions about these terms, please visit our
+ {' '}
website
diff --git a/packages/app-client/src/modules/privacy/pages/privacy.page.tsx b/packages/app-client/src/modules/privacy/pages/privacy.page.tsx
index 60818812..771bd9eb 100644
--- a/packages/app-client/src/modules/privacy/pages/privacy.page.tsx
+++ b/packages/app-client/src/modules/privacy/pages/privacy.page.tsx
@@ -1,7 +1,7 @@
+import type { Component } from 'solid-js';
import { Button } from '@/modules/ui/components/button';
import { Card, CardContent, CardHeader } from '@/modules/ui/components/card';
import { A } from '@solidjs/router';
-import { type Component } from 'solid-js';
export const PrivacyPage: Component = () => {
return (
@@ -66,10 +66,26 @@ export const PrivacyPage: Component = () => {
This instance is hosted using Cloudflare's infrastructure:
-
By using this service, you consent to the collection of this basic analytics data. This helps us improve the service and understand usage patterns. @@ -136,7 +180,8 @@ export const PrivacyPage: Component = () => {