From 477517527f23ad57e5151c87f5c14c5fd44aeea9 Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Sun, 19 Jul 2026 02:50:52 +0300 Subject: [PATCH] chore: add local quality and security gates --- .dialyzer_ignore.exs | 9 ++ .github/workflows/quality.yml | 49 +++++++ Dockerfile | 31 +++- README.md | 15 ++ android/Dockerfile | 38 +++-- compose.e2e.yaml | 2 + compose.quality.yaml | 22 +++ config/config.exs | 1 - config/dev.exs | 2 + config/test.exs | 2 + .../who-need-help/templates/deployments.yaml | 5 + deploy/kind/dependencies.yaml | 39 ++++- docs/dependency-baseline.md | 24 +++ docs/local-hardening-plan.md | 14 ++ docs/verification.md | 22 +++ e2e/Dockerfile | 10 +- .../social_oauth/assent_adapter.ex | 5 +- .../controllers/metrics_controller.ex | 3 + lib/who_need_help_web/router.ex | 8 +- mix.exs | 18 ++- mix.lock | 5 + scripts/android-build.sh | 10 +- scripts/android-instrumentation-test.sh | 4 +- scripts/android-staging-build.sh | 3 +- scripts/backup-compose.sh | 3 +- scripts/bootstrap-admin.sh | 2 +- scripts/bootstrap-kubernetes-tools.sh | 2 +- scripts/codex-review-categories.sh | 2 +- scripts/compose-up.sh | 2 +- scripts/e2e-run.sh | 8 +- scripts/e2e-stop.sh | 2 +- scripts/ensure-local-android-test-env.sh | 2 +- scripts/ensure-local-e2e-env.sh | 2 +- scripts/ensure-local-load-env.sh | 2 +- scripts/ensure-local-metrics-token.sh | 2 +- scripts/ensure-local-public-origin.sh | 3 +- scripts/kind-up.sh | 2 +- scripts/load-run.sh | 5 +- scripts/load-stack-stop.sh | 3 +- scripts/load-stack-up.sh | 3 +- scripts/quality.sh | 137 ++++++++++++++++++ scripts/restore-drill-compose.sh | 5 +- scripts/rotate-local-secrets.sh | 3 +- scripts/test.sh | 3 +- scripts/verify-realtime-cluster.sh | 2 +- 45 files changed, 477 insertions(+), 59 deletions(-) create mode 100644 .dialyzer_ignore.exs create mode 100644 .github/workflows/quality.yml create mode 100644 compose.quality.yaml create mode 100755 scripts/quality.sh diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs new file mode 100644 index 0000000..f94044d --- /dev/null +++ b/.dialyzer_ignore.exs @@ -0,0 +1,9 @@ +# Ecto.Multi stores operation names in a MapSet. OTP 28+ can report +# call_without_opaque at the application call site even though only Ecto's +# public constructors are used. Keep these filters path- and warning-specific; +# `list_unused_filters: true` makes Dialyxir fail when either workaround is no +# longer needed. +[ + {"lib/who_need_help/help.ex", :call_without_opaque}, + {"lib/who_need_help/tracking.ex", :call_without_opaque} +] diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..a900491 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,49 @@ +name: Quality + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: quality-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + backend-quality: + runs-on: ubuntu-24.04 + steps: + - name: Check out the repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Run isolated quality and security gates + run: ./scripts/quality.sh + + browser-e2e: + runs-on: ubuntu-24.04 + steps: + - name: Check out the repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Run isolated browser end-to-end tests + run: ./scripts/e2e-run.sh + + android-build: + runs-on: ubuntu-24.04 + steps: + - name: Check out the repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Build, lint, and unit-test the debug Android app + env: + WNH_ENV_FILE: .env.example + run: ./scripts/android-build.sh diff --git a/Dockerfile b/Dockerfile index 5c6ccac..10b3ffd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,9 @@ FROM ${BUILDER_IMAGE} AS builder # install build dependencies RUN apt-get update \ - && apt-get install -y --no-install-recommends build-essential git \ + && apt-get install -y --no-install-recommends \ + build-essential=12.12 \ + git=1:2.47.3-0+deb13u1 \ && rm -rf /var/lib/apt/lists/* # prepare build dir @@ -88,7 +90,14 @@ RUN mix release FROM ${RUNNER_IMAGE} AS final RUN apt-get update \ - && apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 libsctp1 locales ca-certificates curl \ + && apt-get install -y --no-install-recommends \ + ca-certificates=20250419 \ + curl=8.14.1-2+deb13u4 \ + libncurses6=6.5+20250216-2 \ + libsctp1=1.0.21+dfsg-1 \ + libstdc++6=14.2.0-19 \ + locales=2.41-12+deb13u3 \ + openssl=3.5.6-1~deb13u2 \ && rm -rf /var/lib/apt/lists/* # Set the locale @@ -120,7 +129,10 @@ CMD ["/app/bin/server"] FROM ${BUILDER_IMAGE} AS test RUN apt-get update \ - && apt-get install -y --no-install-recommends build-essential git ca-certificates \ + && apt-get install -y --no-install-recommends \ + build-essential=12.12 \ + ca-certificates=20250419 \ + git=1:2.47.3-0+deb13u1 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -135,7 +147,7 @@ RUN mix deps.get --only test COPY config config RUN mix deps.compile -COPY .formatter.exs ./ +COPY .dialyzer_ignore.exs .formatter.exs ./ COPY priv priv COPY lib lib COPY test test @@ -144,6 +156,15 @@ RUN mix compile CMD ["mix", "test"] +FROM test AS quality + +# PLTs are expensive to create but deterministic for the pinned Erlang, +# Elixir, dependency lock, and test environment. Cache them in this dedicated +# target so every quality command uses the same analyzed dependency set. +RUN mix dialyzer --plt + +CMD ["mix", "dialyzer"] + # Keep the production release as the default build result while exposing the -# dedicated `test` target to scripts/test.sh. +# dedicated `test` and `quality` targets to local verification scripts. FROM final AS release diff --git a/README.md b/README.md index c30f45e..8faf15e 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,21 @@ project's PostGIS service: It creates/updates only the project-scoped `who_need_help_test` database. +The complete local CI-equivalent command uses pinned containerized tools and +an isolated PostgreSQL volume: + +```bash +./scripts/quality.sh +``` + +It checks shell scripts, Dockerfiles, the GitHub Actions workflow, every Compose +profile, the rendered Helm chart, tracked-source secrets and infrastructure +misconfigurations, Elixir formatting/compilation/xref/Credo/Sobelow/Dialyzer, +retired Hex packages, locked npm dependencies, all Phoenix tests, and the +production release image. Its generated database credentials are random and +exist only for that run. The exact database volume, networks, temporary source +snapshot, and one-run images are removed automatically. + The browser E2E command creates a uniquely named, isolated Compose project with its own PostGIS volume, Mailpit instance, Traefik proxy, two web replicas, and two worker replicas: diff --git a/android/Dockerfile b/android/Dockerfile index 5fc019a..ce27c20 100644 --- a/android/Dockerfile +++ b/android/Dockerfile @@ -1,6 +1,7 @@ FROM gradle:9.6.1-jdk17@sha256:7364ce528f33bb6038672bcef990d524f1ad8fbc292935819c235db886d0fae7 AS android-base USER root +SHELL ["/bin/bash", "-o", "pipefail", "-c"] ARG ANDROID_COMMAND_LINE_TOOLS_VERSION=15859902 ARG ANDROID_COMMAND_LINE_TOOLS_SHA1=040d3996a65543d22ec4bf73e4c37aa37a8d4af4 @@ -10,7 +11,9 @@ ENV ANDROID_SDK_ROOT=/opt/android-sdk ENV PATH="${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools" RUN apt-get update \ - && apt-get install -y --no-install-recommends curl unzip \ + && apt-get install -y --no-install-recommends \ + curl=8.18.0-1ubuntu2.3 \ + unzip=6.0-29ubuntu1 \ && rm -rf /var/lib/apt/lists/* RUN mkdir -p "${ANDROID_HOME}/cmdline-tools" \ @@ -28,6 +31,8 @@ RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \ "platforms/android-37.0" \ && chown -R gradle:gradle "${ANDROID_HOME}" +USER gradle + FROM android-base AS android-sdk USER gradle @@ -50,6 +55,7 @@ RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \ FROM android-base AS emulator USER root +SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \ "emulator" \ @@ -58,19 +64,19 @@ RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \ RUN apt-get update \ && apt-get install -y --no-install-recommends \ - libdbus-1-3 \ - libfontconfig1 \ - libgl1 \ - libnss3 \ - libpulse0 \ - libx11-6 \ - libxcomposite1 \ - libxcursor1 \ - libxi6 \ - libxkbfile1 \ - libxrandr2 \ - libxrender1 \ - libxtst6 \ + libdbus-1-3=1.16.2-2ubuntu4 \ + libfontconfig1=2.17.1-3ubuntu1 \ + libgl1=1.7.0-3 \ + libnss3=2:3.120-1ubuntu2.1 \ + libpulse0=1:17.0+dfsg1-2ubuntu4 \ + libx11-6=2:1.8.13-1 \ + libxcomposite1=1:0.4.6-1build1 \ + libxcursor1=1:1.2.3-1build1 \ + libxi6=2:1.8.2-2 \ + libxkbfile1=1:1.1.0-1build5 \ + libxrandr2=2:1.5.4-1build1 \ + libxrender1=1:0.9.12-1build1 \ + libxtst6=2:1.2.5-1build1 \ && rm -rf /var/lib/apt/lists/* # The container is operated from the host through `docker exec ... adb`. @@ -108,6 +114,8 @@ CMD ["/opt/android-sdk/emulator/emulator", \ FROM scratch AS artifact +USER 65532:65532 + COPY --from=android-sdk \ /workspace/android/app/build/outputs/apk/debug/app-debug.apk \ /who-need-help-debug.apk @@ -137,6 +145,8 @@ RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \ FROM scratch AS staging-artifact +USER 65532:65532 + COPY --from=android-staging-sdk \ /workspace/android/app/build/outputs/apk/staging/app-staging.apk \ /who-need-help-staging.apk diff --git a/compose.e2e.yaml b/compose.e2e.yaml index 6eebb8d..cd9a8dd 100644 --- a/compose.e2e.yaml +++ b/compose.e2e.yaml @@ -20,9 +20,11 @@ services: context: e2e init: true ipc: host + user: "${E2E_UID:-1000}:${E2E_GID:-1000}" environment: BASE_URL: ${E2E_BASE_URL:?Set E2E_BASE_URL in the generated E2E environment} MAILPIT_URL: ${E2E_MAILPIT_URL:?Set E2E_MAILPIT_URL in the generated E2E environment} + HOME: /tmp CI: ${CI:-} NO_PROXY: web,mailpit,db,proxy,localhost,127.0.0.1 no_proxy: web,mailpit,db,proxy,localhost,127.0.0.1 diff --git a/compose.quality.yaml b/compose.quality.yaml new file mode 100644 index 0000000..7294f00 --- /dev/null +++ b/compose.quality.yaml @@ -0,0 +1,22 @@ +services: + db: + image: postgis/postgis:18-3.6-alpine@sha256:05d68c7f0f19b9aa0bf7c4a2049b2e8b38b44a63116392b95726a4c913766cf6 + environment: + POSTGRES_DB: postgres + POSTGRES_USER: ${QUALITY_POSTGRES_USER:?Set QUALITY_POSTGRES_USER} + POSTGRES_PASSWORD: ${QUALITY_POSTGRES_PASSWORD:?Set QUALITY_POSTGRES_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 3s + timeout: 3s + retries: 20 + volumes: + - quality_postgres_data:/var/lib/postgresql + networks: [internal] + +networks: + internal: + internal: true + +volumes: + quality_postgres_data: diff --git a/config/config.exs b/config/config.exs index 7152122..510f3b7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -28,7 +28,6 @@ config :who_need_help, ecto_repos: [WhoNeedHelp.Repo], generators: [timestamp_type: :utc_datetime, binary_id: true], app_role: :web, - handover_secret: "development-only-handover-secret", codex_session_id: "not-configured", rate_limit_policies: %{}, map_tile_url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png" diff --git a/config/dev.exs b/config/dev.exs index 1cbdf25..db1f69e 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,5 +1,7 @@ import Config +config :who_need_help, :handover_secret, System.fetch_env!("HANDOVER_SECRET") + # Configure your database config :who_need_help, WhoNeedHelp.Repo, username: "postgres", diff --git a/config/test.exs b/config/test.exs index 3c7d2f4..b4d3cae 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,5 +1,7 @@ import Config +config :who_need_help, :handover_secret, "isolated-test-handover-secret" + # Only in tests, remove the complexity from the password hashing algorithm config :bcrypt_elixir, :log_rounds, 1 diff --git a/deploy/helm/who-need-help/templates/deployments.yaml b/deploy/helm/who-need-help/templates/deployments.yaml index d844a77..9a4fc4b 100644 --- a/deploy/helm/who-need-help/templates/deployments.yaml +++ b/deploy/helm/who-need-help/templates/deployments.yaml @@ -47,6 +47,11 @@ spec: env: - name: APP_ROLE value: migrate + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] containers: - name: {{ $component }} image: "{{ $root.Values.image.repository }}:{{ $root.Values.image.tag }}" diff --git a/deploy/kind/dependencies.yaml b/deploy/kind/dependencies.yaml index f0d7a26..fd1f7c7 100644 --- a/deploy/kind/dependencies.yaml +++ b/deploy/kind/dependencies.yaml @@ -11,10 +11,23 @@ spec: metadata: labels: {app: postgis} spec: + securityContext: + runAsNonRoot: true + runAsUser: 70 + runAsGroup: 70 + fsGroup: 70 + fsGroupChangePolicy: OnRootMismatch + seccompProfile: + type: RuntimeDefault containers: - name: postgis - image: postgis/postgis:18-3.6-alpine + image: postgis/postgis:18-3.6-alpine@sha256:05d68c7f0f19b9aa0bf7c4a2049b2e8b38b44a63116392b95726a4c913766cf6 imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] env: - name: POSTGRES_DB valueFrom: @@ -39,11 +52,17 @@ spec: periodSeconds: 3 volumeMounts: - {name: data, mountPath: /var/lib/postgresql} + - {name: runtime, mountPath: /var/run/postgresql} + - {name: tmp, mountPath: /tmp} volumes: - name: data hostPath: path: /var/local/who-need-help/postgres type: DirectoryOrCreate + - name: runtime + emptyDir: {} + - name: tmp + emptyDir: {} --- apiVersion: v1 kind: Service @@ -66,13 +85,29 @@ spec: metadata: labels: {app: mailpit} spec: + securityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + seccompProfile: + type: RuntimeDefault containers: - name: mailpit - image: axllent/mailpit:v1.30.4 + image: axllent/mailpit:v1.30.4@sha256:5a49a77c5bdbe7c5474450b4f46348d09949df3695257729c93a30369382d4f6 imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] ports: - {name: smtp, containerPort: 1025} - {name: http, containerPort: 8025} + volumeMounts: + - {name: tmp, mountPath: /tmp} + volumes: + - name: tmp + emptyDir: {} --- apiVersion: v1 kind: Service diff --git a/docs/dependency-baseline.md b/docs/dependency-baseline.md index e3ae2a9..fa247d6 100644 --- a/docs/dependency-baseline.md +++ b/docs/dependency-baseline.md @@ -68,6 +68,23 @@ release feeds reported those same versions as current during verification. Android 17 API 37 is the stable target used here. API 37.1 was not selected because the official SDK channel identifies it as a QPR beta. +## Local quality and security tools + +| Component | Pinned version | +| --- | --- | +| ShellCheck | 0.11.0 | +| Hadolint | 2.14.0 | +| actionlint | 1.7.12 | +| Trivy | 0.72.0 | +| Credo | 1.7.19 | +| Dialyxir | 1.4.7 | +| Sobelow | 0.14.1 | +| GitHub checkout action | 7.0.0 / commit `9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0` | + +The four containerized command-line tools are pinned by both exact tag and OCI +digest in `scripts/quality.sh`. The checkout action is pinned by commit in the +workflow. Credo, Dialyxir, and Sobelow are locked in `mix.lock`. + ## Primary version sources - [Elixir releases](https://github.com/elixir-lang/elixir/releases) @@ -91,6 +108,11 @@ because the official SDK channel identifies it as a QPR beta. - [Docker Engine releases](https://github.com/moby/moby/releases) - [Docker Compose releases](https://github.com/docker/compose/releases) - [Docker Buildx releases](https://github.com/docker/buildx/releases) +- [ShellCheck releases](https://github.com/koalaman/shellcheck/releases) +- [Hadolint releases](https://github.com/hadolint/hadolint/releases) +- [actionlint releases](https://github.com/rhysd/actionlint/releases) +- [Trivy releases](https://github.com/aquasecurity/trivy/releases) +- [checkout releases](https://github.com/actions/checkout/releases) ## Repeatable checks @@ -100,6 +122,8 @@ docker build --target node_deps --tag who-need-help:node-deps . docker run --rm who-need-help:node-deps npm --version docker run --rm who-need-help:node-deps npm outdated --json ./scripts/test.sh +./scripts/quality.sh +./scripts/e2e-run.sh ./scripts/android-build.sh ./scripts/android-instrumentation-test.sh ``` diff --git a/docs/local-hardening-plan.md b/docs/local-hardening-plan.md index b3a569f..92ba62e 100644 --- a/docs/local-hardening-plan.md +++ b/docs/local-hardening-plan.md @@ -44,6 +44,20 @@ The goal remains open while any row lacks reproducible local evidence. cleanup, and a disconnected Stop request with visible retry state. It uses an in-process loopback fixture, runs its emulator without external networking, retains diagnostics on failure, and removes its exact container and image. +- The containerized `scripts/quality.sh` gate passes ShellCheck, Hadolint, + actionlint, all Compose renders, Helm lint, a Trivy scan of tracked source and + the rendered Kubernetes manifest, compiler/xref/Credo/Sobelow/Dialyzer/Hex + checks, 149 Phoenix tests, both npm audits, and a Trivy scan of the production + release image. It creates random one-run database credentials and removes its + exact volume, networks, images, and source snapshot. +- The checked-in GitHub Actions workflow runs the same isolated backend/security + gate, browser E2E suite, and Android unit/lint/APK build. The workflow itself + passes actionlint and uses a commit-pinned checkout action without persisted + credentials. +- The hardened local kind manifests also passed a runtime rollout: PostGIS, + Mailpit, both web replicas, and both worker replicas became Ready as non-root + containers with read-only root filesystems, dropped capabilities, and + RuntimeDefault seccomp where configured. The cross-node PubSub probe passed. - The remaining rows above are still pending. Browser accessibility, responsive, offline, and reconnect checks remain part of their respective later rows; this document is not a completion claim. diff --git a/docs/verification.md b/docs/verification.md index 3bee3e0..52509a0 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -28,6 +28,18 @@ results from product limits and unknown production properties. on Elixir 1.20.2 and Erlang/OTP 29.0.3. - `mix compile --force --warnings-as-errors` and `mix format --check-formatted`: passed against the same final source. +- `./scripts/quality.sh` passed ShellCheck 0.11.0, Hadolint 2.14.0 at warning + threshold, actionlint 1.7.12, all four Compose renders, Helm lint, Trivy + source/rendered-manifest scanning, xref, Credo high-priority checks, Sobelow + strict/private checks, Hex audit, 149 Phoenix tests, both npm audits, and the + production-image vulnerability scan. The rendered Helm manifest and Debian + 13.6 release image each reported zero HIGH/CRITICAL findings under the + configured gates. +- Dialyzer passed with two exact, documented filters and zero unused filters. + Both findings are opaque-type warnings at Ecto `Multi`/`MapSet` call sites; + the filters are visible in `.dialyzer_ignore.exs` rather than hidden by a + broad pattern. This is not a claim that third-party dependency compilation is + warning-free. - The generated Activity migration was rolled back by exactly one step and migrated forward again against `who_need_help_test`; both directions passed. - The Activity-report migration was also rolled back and migrated forward. The @@ -45,6 +57,16 @@ results from product limits and unknown production properties. intended; release configuration accepts only an explicit HTTPS origin. - Helm lint, template rendering, server-side dry-run, rollout waits, readiness checks, and cross-node PubSub verification passed in the local kind cluster. +- The hardened kind rollout then ran PostGIS as UID/GID 70 and Mailpit as + UID/GID 10001. Both used read-only root filesystems, dropped all Linux + capabilities, disabled privilege escalation, and used RuntimeDefault seccomp. + Both web and both worker replicas were Ready; their migration-wait init + containers also used read-only root filesystems and dropped capabilities. + The cross-node probe observed all four BEAM nodes, and live/readiness returned + `ok`/`ready`. Database counts remained `0 users / 0 help requests`. +- The committed browser suite was repeated after CSP and non-root E2E-runner + changes: its bootstrap test passed 1/1 and its three Chromium scenarios + passed 3/3. The uniquely named Compose volume and networks were removed. - Browser verification used headed Chrome. The authenticated matched-request page rendered its chat, MapLibre marker, and live-location controls with no console errors or warnings after the foreground-service rollout. diff --git a/e2e/Dockerfile b/e2e/Dockerfile index 8cfb497..b5e4197 100644 --- a/e2e/Dockerfile +++ b/e2e/Dockerfile @@ -2,11 +2,13 @@ FROM mcr.microsoft.com/playwright:v1.61.1-noble@sha256:5b8f294aff9041b7191c34a4b WORKDIR /work -COPY package.json package-lock.json ./ +COPY --chown=pwuser:pwuser package.json package-lock.json ./ RUN npm ci --ignore-scripts -COPY playwright.config.ts playwright.bootstrap.config.ts ./ -COPY tests tests -COPY setup setup +COPY --chown=pwuser:pwuser playwright.config.ts playwright.bootstrap.config.ts ./ +COPY --chown=pwuser:pwuser tests tests +COPY --chown=pwuser:pwuser setup setup + +USER pwuser CMD ["npx", "playwright", "test"] diff --git a/lib/who_need_help/social_oauth/assent_adapter.ex b/lib/who_need_help/social_oauth/assent_adapter.ex index 10197c3..11ca6b5 100644 --- a/lib/who_need_help/social_oauth/assent_adapter.ex +++ b/lib/who_need_help/social_oauth/assent_adapter.ex @@ -21,9 +21,8 @@ defmodule WhoNeedHelp.SocialOAuth.AssentAdapter do {:ok, %{user: user}} <- config |> Keyword.put(:session_params, session_params) - |> GithubStrategy.callback(params), - {:ok, identity} <- normalize_github_identity(user) do - {:ok, identity} + |> GithubStrategy.callback(params) do + normalize_github_identity(user) end end diff --git a/lib/who_need_help_web/controllers/metrics_controller.ex b/lib/who_need_help_web/controllers/metrics_controller.ex index aef95b2..aea0839 100644 --- a/lib/who_need_help_web/controllers/metrics_controller.ex +++ b/lib/who_need_help_web/controllers/metrics_controller.ex @@ -3,6 +3,9 @@ defmodule WhoNeedHelpWeb.MetricsController do @content_type "text/plain; version=0.0.4" + # Sobelow's HTML-oriented SendResp check cannot infer this fixed Prometheus + # text content type. The response is not rendered in an HTML context. + # sobelow_skip ["XSS.SendResp"] def show(conn, _params) do token = Application.get_env(:who_need_help, :metrics_token) diff --git a/lib/who_need_help_web/router.ex b/lib/who_need_help_web/router.ex index 05044ff..72cfdf0 100644 --- a/lib/who_need_help_web/router.ex +++ b/lib/who_need_help_web/router.ex @@ -3,13 +3,17 @@ defmodule WhoNeedHelpWeb.Router do import WhoNeedHelpWeb.UserAuth + @secure_browser_headers %{ + "content-security-policy" => "base-uri 'self'; frame-ancestors 'self';" + } + pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_live_flash plug :put_root_layout, html: {WhoNeedHelpWeb.Layouts, :root} plug :protect_from_forgery - plug :put_secure_browser_headers + plug :put_secure_browser_headers, @secure_browser_headers plug :fetch_current_scope_for_user plug WhoNeedHelpWeb.Locale end @@ -22,7 +26,7 @@ defmodule WhoNeedHelpWeb.Router do plug :accepts, ["json"] plug :fetch_session plug :protect_from_forgery - plug :put_secure_browser_headers + plug :put_secure_browser_headers, @secure_browser_headers plug :fetch_current_scope_for_user end diff --git a/mix.exs b/mix.exs index faa9ae0..254ab19 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,12 @@ defmodule WhoNeedHelp.MixProject do aliases: aliases(), deps: deps(), compilers: [:phoenix_live_view] ++ Mix.compilers(), - listeners: [Phoenix.CodeReloader] + listeners: [Phoenix.CodeReloader], + dialyzer: [ + plt_add_apps: [:ex_unit, :mix], + ignore_warnings: ".dialyzer_ignore.exs", + list_unused_filters: true + ] ] end @@ -51,6 +56,9 @@ defmodule WhoNeedHelp.MixProject do {:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_view, "~> 1.2.0"}, {:lazy_html, ">= 0.1.0", only: :test}, + {:credo, "~> 1.7.19", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.4.7", only: [:dev, :test], runtime: false}, + {:sobelow, "~> 0.14.1", only: [:dev, :test], runtime: false}, {:phoenix_live_dashboard, "~> 0.8.3"}, {:esbuild, "~> 0.10", runtime: Mix.env() == :dev}, {:tailwind, "~> 0.5", runtime: Mix.env() == :dev}, @@ -101,7 +109,13 @@ defmodule WhoNeedHelp.MixProject do "esbuild who_need_help --minify", "phx.digest" ], - precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"] + precommit: [ + "compile --warnings-as-errors", + "format --check-formatted", + "credo --strict --min-priority high", + "sobelow --exit --strict --private --skip", + "test" + ] ] end end diff --git a/mix.lock b/mix.lock index 2d99885..2bc5651 100644 --- a/mix.lock +++ b/mix.lock @@ -2,15 +2,19 @@ "assent": {:hex, :assent, "0.3.1", "7e7b04f4ab5d07b497b80c04f009a0f1efc409787e68b8e086512b9098f3095b", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "3597b31f9eb556d97e64cf60c00d3451f7353d7b465a71d33530b870ebed1ff1"}, "bandit": {:hex, :bandit, "1.12.0", "6c5214daa2469644ac4ab0113b98abc24f75e348378e6a974c6343b3e5da22ef", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "45dac82dc86f45cf4a196dee9cc5a8b791d9c9469d996055f055e6ee36c66e20"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, + "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, "daisyui": {:git, "https://github.com/saadeghi/daisyui.git", "374d63758f997317e92e93e547569a5f9ad11250", [tag: "v5.6.18", sparse: "packages/bundle", depth: 1]}, "db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"}, "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, + "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, "ecto": {:hex, :ecto, "3.14.1", "7b740d87bdf45996aa0c2c2e081640906f10caa7ce5ba328fd294c7d49d0cc6f", [:mix], [{:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "24b991956796700f467d0a3ef3d303138a3ef9ddddf8b98f43758ee067b20a30"}, "ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, + "erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, @@ -43,6 +47,7 @@ "postgrex": {:hex, :postgrex, "0.22.3", "bf65941737ee7a9adbe4a64c91080310d11703da343e8ac9188aacb9eb9f6f02", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "f018c13752b2b46e8d35d7e2d84c3276557cbfd880769109021a1d0ee36c1cfe"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "req": {:hex, :req, "0.6.3", "7fe5e68792ff0546e45d5919104fa1764a13694cfe3e48c8a0f32ad051ae77e4", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "e85b5c6c990e6c3f52bbba68e6f099118f2b8252825f96c7c3636b97a3de307d"}, + "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, "swoosh": {:hex, :swoosh, "1.26.3", "9d8b60077305ce259298d9a1102e5be67cd3c41d1ea930c29e9288af195ca017", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c7683d070fe8f8aa9d174e61b01f2d527be73cd8ac40037b7109184941eb569f"}, "tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, diff --git a/scripts/android-build.sh b/scripts/android-build.sh index c775eac..d60b032 100755 --- a/scripts/android-build.sh +++ b/scripts/android-build.sh @@ -1,8 +1,13 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -ENV_FILE="$ROOT/.env" +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +ENV_FILE=${WNH_ENV_FILE:-"$ROOT/.env"} + +case "$ENV_FILE" in + /*) ;; + *) ENV_FILE="$ROOT/$ENV_FILE" ;; +esac if [ ! -f "$ENV_FILE" ]; then echo "Missing $ENV_FILE. Copy .env.example to .env and configure WNH_DEBUG_BASE_URL." >&2 @@ -10,6 +15,7 @@ if [ ! -f "$ENV_FILE" ]; then fi set -a +# shellcheck source=/dev/null . "$ENV_FILE" set +a diff --git a/scripts/android-instrumentation-test.sh b/scripts/android-instrumentation-test.sh index b520842..82af921 100755 --- a/scripts/android-instrumentation-test.sh +++ b/scripts/android-instrumentation-test.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) ANDROID_ENV="$ROOT/.env" TEST_ENV="$ROOT/.env.android-test" run_id=$(date -u +%Y%m%d%H%M%S)-$$ @@ -23,7 +23,9 @@ fi "$ROOT/scripts/ensure-local-android-test-env.sh" set -a +# shellcheck source=/dev/null . "$ANDROID_ENV" +# shellcheck source=/dev/null . "$TEST_ENV" set +a diff --git a/scripts/android-staging-build.sh b/scripts/android-staging-build.sh index 5ef2261..dbfed29 100755 --- a/scripts/android-staging-build.sh +++ b/scripts/android-staging-build.sh @@ -1,12 +1,13 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) ENV_FILE="$ROOT/.env" "$ROOT/scripts/ensure-local-public-origin.sh" set -a +# shellcheck source=/dev/null . "$ENV_FILE" set +a diff --git a/scripts/backup-compose.sh b/scripts/backup-compose.sh index df82460..4d0b692 100755 --- a/scripts/backup-compose.sh +++ b/scripts/backup-compose.sh @@ -2,7 +2,7 @@ set -eu umask 077 -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) cd "$ROOT" if [ ! -f "$ROOT/.env" ]; then @@ -11,6 +11,7 @@ if [ ! -f "$ROOT/.env" ]; then fi set -a +# shellcheck source=/dev/null . "$ROOT/.env" set +a diff --git a/scripts/bootstrap-admin.sh b/scripts/bootstrap-admin.sh index 1c67b62..8e67186 100755 --- a/scripts/bootstrap-admin.sh +++ b/scripts/bootstrap-admin.sh @@ -24,7 +24,7 @@ case "$MODE" in docker exec "$TARGET" /app/bin/who_need_help rpc "$EXPRESSION" ;; kind) - ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) + ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) PATH="$ROOT/.tools/bin:$PATH" export PATH TARGET=$( diff --git a/scripts/bootstrap-kubernetes-tools.sh b/scripts/bootstrap-kubernetes-tools.sh index 5b8dfc5..796b45f 100755 --- a/scripts/bootstrap-kubernetes-tools.sh +++ b/scripts/bootstrap-kubernetes-tools.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) TOOLS="$ROOT/.tools/bin" mkdir -p "$TOOLS" diff --git a/scripts/codex-review-categories.sh b/scripts/codex-review-categories.sh index 543a314..6657e3c 100755 --- a/scripts/codex-review-categories.sh +++ b/scripts/codex-review-categories.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) INPUT=${1:-} OUTPUT=${2:-} diff --git a/scripts/compose-up.sh b/scripts/compose-up.sh index bc4198b..d6c6166 100755 --- a/scripts/compose-up.sh +++ b/scripts/compose-up.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) cd "$ROOT" CODEX_SESSION_ID=${CODEX_SESSION_ID:-${CODEX_THREAD_ID:-not-configured}} export CODEX_SESSION_ID diff --git a/scripts/e2e-run.sh b/scripts/e2e-run.sh index 918f832..bd9c3e9 100755 --- a/scripts/e2e-run.sh +++ b/scripts/e2e-run.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) cd "$ROOT" "$ROOT/scripts/ensure-local-e2e-env.sh" @@ -24,7 +24,11 @@ export TRAEFIK_PROJECT_CONSTRAINT="$project" export TRAEFIK_APP_NAME="who-need-help-e2e-$run_id" export TRAEFIK_DOCKER_NETWORK="${project}_internal" export E2E_OUTPUT_DIR="$output_dir" -export CODEX_SESSION_ID=${CODEX_SESSION_ID:-${CODEX_THREAD_ID:-local-e2e}} +export E2E_UID +E2E_UID=$(id -u) +export E2E_GID +E2E_GID=$(id -g) +export CODEX_SESSION_ID="${CODEX_SESSION_ID:-${CODEX_THREAD_ID:-local-e2e}}" compose() { docker compose \ diff --git a/scripts/e2e-stop.sh b/scripts/e2e-stop.sh index 4c93e9a..1469c4b 100755 --- a/scripts/e2e-stop.sh +++ b/scripts/e2e-stop.sh @@ -16,7 +16,7 @@ case "$project" in ;; esac -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) docker compose \ --project-name "$project" \ diff --git a/scripts/ensure-local-android-test-env.sh b/scripts/ensure-local-android-test-env.sh index aced2a3..37c109c 100755 --- a/scripts/ensure-local-android-test-env.sh +++ b/scripts/ensure-local-android-test-env.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) target="$ROOT/.env.android-test" if [ -f "$target" ]; then diff --git a/scripts/ensure-local-e2e-env.sh b/scripts/ensure-local-e2e-env.sh index 727c728..466eab9 100755 --- a/scripts/ensure-local-e2e-env.sh +++ b/scripts/ensure-local-e2e-env.sh @@ -2,7 +2,7 @@ set -eu umask 077 -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) target="$ROOT/.env.e2e" if [ -f "$target" ]; then diff --git a/scripts/ensure-local-load-env.sh b/scripts/ensure-local-load-env.sh index dafd41c..3bd3938 100755 --- a/scripts/ensure-local-load-env.sh +++ b/scripts/ensure-local-load-env.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) TEMPLATE="$ROOT/.env.load.example" ENV_FILE="$ROOT/.env.load" diff --git a/scripts/ensure-local-metrics-token.sh b/scripts/ensure-local-metrics-token.sh index 6371d1b..2fe3b4e 100755 --- a/scripts/ensure-local-metrics-token.sh +++ b/scripts/ensure-local-metrics-token.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) ENV_FILE="$ROOT/.env" if [ ! -f "$ENV_FILE" ]; then diff --git a/scripts/ensure-local-public-origin.sh b/scripts/ensure-local-public-origin.sh index 59e801e..5214f02 100755 --- a/scripts/ensure-local-public-origin.sh +++ b/scripts/ensure-local-public-origin.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) ENV_FILE="$ROOT/.env" if [ ! -f "$ENV_FILE" ]; then @@ -15,6 +15,7 @@ if ! command -v perl >/dev/null 2>&1; then fi set -a +# shellcheck source=/dev/null . "$ENV_FILE" set +a diff --git a/scripts/kind-up.sh b/scripts/kind-up.sh index 7fd3bd2..9c75ca9 100755 --- a/scripts/kind-up.sh +++ b/scripts/kind-up.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) "$ROOT/scripts/bootstrap-kubernetes-tools.sh" PATH="$ROOT/.tools/bin:$PATH" export PATH diff --git a/scripts/load-run.sh b/scripts/load-run.sh index 5258579..f88bb69 100755 --- a/scripts/load-run.sh +++ b/scripts/load-run.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) ENV_FILE="$ROOT/.env.load" K6_IMAGE="grafana/k6@sha256:65c920dc067d5e2e00befbf982af6ad6ad0117034e8b1c65817c7975c52d4669" LABEL=${1:-"run-$(date -u +%Y%m%dT%H%M%SZ)"} @@ -12,6 +12,7 @@ if [[ ! -f "$ENV_FILE" ]]; then fi set -a +# shellcheck source=/dev/null . "$ENV_FILE" set +a @@ -81,6 +82,8 @@ touch "$running_marker" snapshot_database() { local destination=$1 + # The variables below are intentionally expanded inside the database container. + # shellcheck disable=SC2016 "${compose[@]}" exec -T db sh -c \ 'psql --no-psqlrc --set ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB"' \ >"$destination" <<'SQL' diff --git a/scripts/load-stack-stop.sh b/scripts/load-stack-stop.sh index 23ef64f..1860b93 100755 --- a/scripts/load-stack-stop.sh +++ b/scripts/load-stack-stop.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) ENV_FILE="$ROOT/.env.load" if [ ! -f "$ENV_FILE" ]; then @@ -10,6 +10,7 @@ if [ ! -f "$ENV_FILE" ]; then fi set -a +# shellcheck source=/dev/null . "$ENV_FILE" set +a diff --git a/scripts/load-stack-up.sh b/scripts/load-stack-up.sh index e7f9c19..8036a32 100755 --- a/scripts/load-stack-up.sh +++ b/scripts/load-stack-up.sh @@ -1,13 +1,14 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) ENV_FILE="$ROOT/.env.load" REPLICAS=${1:-} "$ROOT/scripts/ensure-local-load-env.sh" set -a +# shellcheck source=/dev/null . "$ENV_FILE" set +a diff --git a/scripts/quality.sh b/scripts/quality.sh new file mode 100755 index 0000000..d0a2cdc --- /dev/null +++ b/scripts/quality.sh @@ -0,0 +1,137 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +cd "$ROOT" + +SHELLCHECK_IMAGE="koalaman/shellcheck-alpine:v0.11.0@sha256:9955be09ea7f0dbf7ae942ac1f2094355bb30d96fffba0ec09f5432207544002" +HADOLINT_IMAGE="hadolint/hadolint:v2.14.0-debian@sha256:158cd0184dcaa18bd8ec20b61f4c1cabdf8b32a592d062f57bdcb8e4c1d312e2" +ACTIONLINT_IMAGE="rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667" +TRIVY_IMAGE="aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f" + +run_id="$(date -u +%Y%m%d%H%M%S)-$$" +project="wnh_quality_$(printf '%s' "$run_id" | tr -d '-')" +quality_image="who-need-help:quality-$run_id" +assets_image="who-need-help:assets-audit-$run_id" +e2e_image="who-need-help:e2e-audit-$run_id" +release_image="who-need-help:security-$run_id" +scan_dir=$(mktemp -d "${TMPDIR:-/tmp}/wnh-quality-scan.XXXXXX") +scan_list="${scan_dir}.files" +scan_tar="${scan_dir}.tar" + +umask 077 +QUALITY_POSTGRES_USER="wnh_quality_$(openssl rand -hex 6)" +QUALITY_POSTGRES_PASSWORD=$(openssl rand -base64 48 | tr -d '\n') +export QUALITY_POSTGRES_USER QUALITY_POSTGRES_PASSWORD + +compose="docker compose -p $project -f $ROOT/compose.quality.yaml" + +cleanup() { + $compose down --volumes --remove-orphans >/dev/null 2>&1 || true + docker image rm "$quality_image" "$assets_image" "$e2e_image" "$release_image" \ + >/dev/null 2>&1 || true + rm -rf "$scan_dir" "$scan_list" "$scan_tar" +} +trap cleanup EXIT HUP INT TERM + +echo "Checking shell scripts with ShellCheck 0.11.0" +# Word splitting is intentional: find emits repository-controlled paths and +# ShellCheck expects each file as a separate argument. +# shellcheck disable=SC2046 +docker run --rm \ + --volume "$ROOT:/mnt:ro" \ + --workdir /mnt \ + --entrypoint shellcheck \ + "$SHELLCHECK_IMAGE" \ + $(find scripts -type f -name '*.sh' -print | sort) + +echo "Checking Dockerfiles with Hadolint 2.14.0" +for dockerfile in Dockerfile android/Dockerfile e2e/Dockerfile; do + docker run --rm --interactive "$HADOLINT_IMAGE" \ + hadolint --failure-threshold warning - <"$dockerfile" +done + +echo "Checking the GitHub Actions workflow with actionlint 1.7.12" +docker run --rm \ + --volume "$ROOT:/repo:ro" \ + --workdir /repo \ + "$ACTIONLINT_IMAGE" + +echo "Rendering every Docker Compose profile" +docker compose --env-file .env.example -f compose.yaml config --quiet +mkdir -p "$scan_dir/e2e-output" +E2E_OUTPUT_DIR="$scan_dir/e2e-output" docker compose --env-file .env.e2e.example \ + -f compose.yaml -f compose.e2e.yaml config --quiet +docker compose --env-file .env.load.example \ + -f compose.yaml -f compose.load.yaml config --quiet +docker compose -p "$project" -f compose.quality.yaml config --quiet + +echo "Linting the Helm chart" +"$ROOT/scripts/bootstrap-kubernetes-tools.sh" >/dev/null +"$ROOT/.tools/bin/helm" lint \ + --values "$ROOT/deploy/helm/who-need-help/values-kind.yaml" \ + "$ROOT/deploy/helm/who-need-help" + +echo "Scanning only tracked and non-ignored source files" +git ls-files --cached --others --exclude-standard -z >"$scan_list" +tar --null --no-recursion --files-from="$scan_list" --create --file="$scan_tar" +tar --extract --file="$scan_tar" --directory "$scan_dir" +"$ROOT/.tools/bin/helm" template who-need-help \ + --values "$ROOT/deploy/helm/who-need-help/values-kind.yaml" \ + "$ROOT/deploy/helm/who-need-help" \ + >"$scan_dir/rendered-helm.yaml" +mkdir -p "$ROOT/.tools/trivy-cache" +docker run --rm \ + --volume "$scan_dir:/scan:ro" \ + --volume "$ROOT/.tools/trivy-cache:/root/.cache/trivy" \ + "$TRIVY_IMAGE" fs \ + --scanners misconfig,secret \ + --severity HIGH,CRITICAL \ + --exit-code 1 \ + /scan + +echo "Building the pinned quality image and cached Dialyzer PLTs" +docker build --target quality --tag "$quality_image" . + +echo "Running Elixir format, compiler, xref, Credo, Sobelow, Dialyzer, and Hex audit" +docker run --rm "$quality_image" sh -euc ' + mix format --check-formatted + mix compile --force --warnings-as-errors + mix xref graph --label compile-connected --fail-above 0 + mix credo --strict --min-priority high + mix sobelow --exit --strict --private --skip + mix dialyzer + mix hex.audit +' + +echo "Starting an isolated PostgreSQL/PostGIS volume for the Phoenix suite" +$compose up --detach --wait db +docker run --rm \ + --network "${project}_internal" \ + --env MIX_ENV=test \ + --env DB_HOST=db \ + --env "DB_USER=$QUALITY_POSTGRES_USER" \ + --env "DB_PASSWORD=$QUALITY_POSTGRES_PASSWORD" \ + --env TEST_POOL_SIZE=10 \ + "$quality_image" \ + mix test + +echo "Auditing locked browser dependencies" +docker build --target node_deps --tag "$assets_image" . +docker run --rm "$assets_image" npm audit --audit-level=high +docker build --tag "$e2e_image" e2e +docker run --rm "$e2e_image" npm audit --audit-level=high + +echo "Building and scanning the production release image" +docker build --target release --tag "$release_image" . +docker run --rm \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + --volume "$ROOT/.tools/trivy-cache:/root/.cache/trivy" \ + "$TRIVY_IMAGE" image \ + --scanners vuln \ + --severity HIGH,CRITICAL \ + --ignore-unfixed \ + --exit-code 1 \ + "$release_image" + +echo "All isolated quality and security gates passed." diff --git a/scripts/restore-drill-compose.sh b/scripts/restore-drill-compose.sh index 55d1157..c014bc5 100755 --- a/scripts/restore-drill-compose.sh +++ b/scripts/restore-drill-compose.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) cd "$ROOT" if [ "$#" -ne 1 ]; then @@ -28,6 +28,7 @@ if [ ! -f "$ROOT/.env" ]; then fi set -a +# shellcheck source=/dev/null . "$ROOT/.env" set +a @@ -35,7 +36,7 @@ set +a : "${POSTGRES_USER:?Set POSTGRES_USER in .env}" : "${DATABASE_URL:?Set DATABASE_URL in .env}" -dump_dir=$(CDPATH= cd -- "$(dirname -- "$dump")" && pwd) +dump_dir=$(CDPATH='' cd -- "$(dirname -- "$dump")" && pwd) dump_name=$(basename -- "$dump") ( diff --git a/scripts/rotate-local-secrets.sh b/scripts/rotate-local-secrets.sh index 5809b8d..0688aed 100755 --- a/scripts/rotate-local-secrets.sh +++ b/scripts/rotate-local-secrets.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) ENV_FILE="$ROOT/.env" TEST_IMAGE="${TEST_IMAGE:-who-need-help:test}" @@ -18,6 +18,7 @@ for command in docker openssl perl; do done set -a +# shellcheck source=/dev/null . "$ENV_FILE" set +a diff --git a/scripts/test.sh b/scripts/test.sh index 04bf38a..a413e08 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) cd "$ROOT" if [ ! -f "$ROOT/.env" ]; then @@ -10,6 +10,7 @@ if [ ! -f "$ROOT/.env" ]; then fi set -a +# shellcheck source=/dev/null . "$ROOT/.env" set +a diff --git a/scripts/verify-realtime-cluster.sh b/scripts/verify-realtime-cluster.sh index 95890b3..0cd33b8 100755 --- a/scripts/verify-realtime-cluster.sh +++ b/scripts/verify-realtime-cluster.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) MODE=${1:-compose} COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-who_need_help}