diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..286f9d079 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,73 @@ +name: CodeQL + +# Static analysis for the TypeScript tree. This is GitHub's default setup +# expressed as a workflow file rather than enabled from the repository +# settings, so the query suite, the excluded paths and the schedule are +# reviewable in the diff like everything else here. +# +# Deliberately NOT on `push: main`. The weekly run already analyses the +# default branch, which is what keeps the Security tab current, and every +# change reaches `main` through a pull request that is analysed on the way +# in. Adding a push trigger would analyse the same tree a second time +# minutes later for no additional signal. +on: + pull_request: + branches: [main] + schedule: + # Mondays, 04:27 UTC. Off the hour so the run does not queue behind + # everyone else's midnight cron. + - cron: "27 4 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyse javascript-typescript + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + # Required to write the results into the repository's code-scanning + # alerts. Free for public repositories. + security-events: write + # Required by the CodeQL action to read workflow run metadata. + actions: read + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # `build-mode: none` is the correct mode for JavaScript/TypeScript — + # CodeQL extracts from source and needs neither a compile step nor + # installed dependencies. + # + # The excluded paths are all generated or vendored output. Chief + # among them is `src/generated`, the ~9 MB Prisma client with a + # 224 KB inline schema literal in it: extracting that costs real + # minutes and can produce nothing actionable, because nobody edits + # it and the fix for anything found there is `pnpm prisma generate`. + - name: Initialise CodeQL + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + with: + languages: javascript-typescript + build-mode: none + config: | + paths-ignore: + - src/generated + - .next + - dist + - coverage + - playwright-report + - test-results + - public + + - name: Analyse + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + with: + category: "/language:javascript-typescript" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 000000000..0fb4bcd82 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,60 @@ +name: OpenSSF Scorecard + +# Automated checks against the OpenSSF's supply-chain criteria: are Actions +# pinned by SHA, is there a security policy, are dependencies updated +# automatically, is the release pipeline signed, is there a fuzzing or a +# static-analysis story. Most of what it looks for this repository already +# does; the value is that it keeps looking after the person who set it up +# has moved on to something else. +# +# `publish_results: true` sends the result to the OpenSSF's public API, +# which is what makes the score citable by anyone evaluating the project +# without asking for CI access. Nothing about the code or its contents +# leaves; the checks read repository metadata and the workflow files. +on: + # Re-score when a branch-protection rule changes, since several checks + # read exactly that. + branch_protection_rule: + schedule: + # Tuesdays, 07:20 UTC. + - cron: "20 7 * * 2" + workflow_dispatch: + +permissions: read-all + +jobs: + analysis: + name: Score supply-chain practices + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + # Write the findings into the repository's code-scanning alerts. + security-events: write + # Publish the result to the public API. This is an OIDC token + # exchange, the same mechanism the release workflow uses for keyless + # image signing; no long-lived credential is involved. + id-token: write + contents: read + actions: read + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Scorecard reads the repository through the API, not through the + # checkout, and leaving the token in `.git/config` would hand it + # to anything else running in the job. + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: scorecard-results.sarif + results_format: sarif + publish_results: true + + - name: Upload results + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + with: + sarif_file: scorecard-results.sarif + category: scorecard diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 000000000..235c8ea63 --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,164 @@ +name: Trivy Scheduled Scan + +# Two scans that the per-change gates cannot do, on a daily cadence. +# +# What already exists and is NOT repeated here: +# - `security.yml` → `dependency-audit` runs `pnpm audit --prod` on every +# push and pull request, blocking on HIGH/CRITICAL. +# - `security.yml` → `docker-security` builds the image on a push to `main` +# and runs `trivy image` against it, blocking on HIGH/CRITICAL. +# +# Both are change-triggered, and both answer "is this change safe". Neither +# answers "is what we shipped last week still safe", because an advisory +# published today lands against an image nobody is about to rebuild. That is +# what the schedule is for. There is deliberately no push or pull_request +# trigger: adding one would run the same scan the `docker-security` job +# already ran, minutes later, on the same tree. +on: + schedule: + # Daily, 05:17 UTC. + - cron: "17 5 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: trivy-scheduled + cancel-in-progress: true + +jobs: + filesystem: + name: Lockfile (including dev dependencies) + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + security-events: write + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # `pnpm audit --prod` is the blocking gate and it is scoped to + # production dependencies on purpose: a dev-only advisory should not + # stop a release. The gap that scoping leaves is that "dev dependency" + # and "not in the image" are not the same statement. + # + # `pdfjs-dist` is the worked example. It sits in `devDependencies`, + # and `src/lib/documents/rasterize-pdf.ts` imports it at runtime, so + # Next's standalone output tracing copies it into the production + # image. `pnpm audit --prod` cannot see it. This scan can: without + # the flag Trivy reads 627 packages out of `pnpm-lock.yaml`, with it + # 1455, and `pdfjs-dist` is only in the second set. + # + # Trivy's own `--help` lists the flag as supported for npm, yarn and + # gradle. It works on `pnpm-lock.yaml` too; the help text is behind + # the parser. Verified against this repository before this workflow + # was written, and worth re-checking if the package count ever + # collapses back to the production figure. + # + # Report-only, and that is a deliberate choice rather than a soft + # one. The findings this widening surfaces are overwhelmingly + # transitive dev-tool advisories with fixes available upstream; + # failing the run on them from day one produces a red badge that gets + # muted within a week, and a muted signal is worse than none. The + # SARIF upload puts every finding in the repository's code-scanning + # alerts, which persist, notify, and can be triaged one at a time. + # The blocking answer to "does this ship" stays where it is, in + # `security.yml`. + - name: Trivy filesystem scan + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + env: + # No action input maps to `--include-dev-deps`; Trivy reads every + # flag from a `TRIVY_`-prefixed environment variable, so this is + # the documented way to reach it through the action. + TRIVY_INCLUDE_DEV_DEPS: "true" + with: + scan-type: fs + scan-ref: . + scanners: vuln + format: sarif + output: trivy-filesystem.sarif + severity: "CRITICAL,HIGH,MEDIUM" + limit-severities-for-sarif: true + exit-code: "0" + # Generated Prisma client. Nobody edits it, the fix for anything + # found in it is `pnpm prisma generate`, and it is ~9 MB. + skip-dirs: src/generated + + - name: Upload results + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + with: + sarif_file: trivy-filesystem.sarif + category: trivy-filesystem + + published-image: + name: Published image + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + packages: read + security-events: write + + steps: + # The `docker-security` job scans an image it just built from the + # current `main`. This one scans the image operators actually pull, + # which is the one built from the last release tag and unchanged + # since. The two answer different questions and the gap between them + # widens every day a release stays out. + # + # Trivy resolves the multi-arch manifest to the runner's platform, so + # this covers `linux/amd64`. The arm64 manifest shares every + # application layer and differs only in native binaries. + - name: Log in to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # `.trivyignore` lives at the repository root and Trivy reads it + # automatically, so the suppressions the blocking gate already + # carries apply here too and the two scans cannot disagree about + # what is accepted. + - name: Checkout for .trivyignore + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # Fails on CRITICAL only. A critical advisory against the image + # self-hosters are running right now is worth a red badge and a + # notification; HIGH goes to code scanning with everything else, + # because the release pipeline already refuses to publish an image + # carrying one and anything that appears here appeared after the + # fact. + - name: Trivy published-image scan + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: image + image-ref: ghcr.io/mbombeck/healthlog:latest + scanners: vuln + format: sarif + output: trivy-published-image.sarif + severity: "CRITICAL,HIGH,MEDIUM" + limit-severities-for-sarif: true + exit-code: "0" + + - name: Upload results + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + with: + sarif_file: trivy-published-image.sarif + category: trivy-published-image + + # Separate table pass so the SARIF upload above always runs, even + # when this one fails the job. A scan whose failure suppresses its + # own report is a scan you have to re-run by hand to read. + - name: Fail on a critical finding in the published image + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: image + image-ref: ghcr.io/mbombeck/healthlog:latest + scanners: vuln + format: table + severity: "CRITICAL" + exit-code: "1"