Files
backspace/docs/superpowers/plans/2026-07-10-plan-a-scanning-pipeline.md
T
Jannis Braun aa5052ba8a docs(security): add scanning/hardening design spec + Plan A implementation plan
Completes the design record on main: Plan B's plan and the federation spec were
already here; this adds the umbrella security spec (source of truth for the
remaining container/web/desktop/remediation workstreams) and Plan A's plan.
2026-07-13 11:36:23 +02:00

25 KiB

Plan A — Scanning Pipeline & Supply-Chain Hardening (report-only) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Stand up the full automated security-scanning pipeline (Dependabot, CodeQL SAST, secret scanning, dependency CVEs, IaC/license scanning, OpenSSF Scorecard) plus supply-chain hardening (SHA-pinned actions, harden-runner, least-privilege permissions) on GitHub Actions — all report-only/advisory, so the PR that adds it stays green and mergeable.

Architecture: Four new files under .github/ (one Dependabot config + three workflows) each with a single scan responsibility, plus a hardening sweep across the four existing workflows. Every scanner uploads SARIF to the GitHub Security tab and is non-blocking in this plan; enforcement (fail-the-build) is flipped on in a later plan (Plan E) after the remediation pass. This is the foundation the rest of the initiative builds on and, on its own, answers the "no security scanning" objection with visibly-running scanners.

Tech Stack: GitHub Actions (YAML), GitHub Dependabot, CodeQL (javascript-typescript, build-mode none), gitleaks, OSV-Scanner (reads pnpm-lock.yaml v9), Aqua Trivy (config + license), OpenSSF Scorecard, StepSecurity harden-runner. Local validators: actionlint, pinact (SHA-pinning).

Global Constraints

  • Report-only in this plan. Every scanner must be non-blocking (continue-on-error: true at step level, or advisory SARIF upload). Enforcement is flipped on in Plan E — do NOT make any scanner fail the build here. Each non-blocking step carries a comment: # report-only; enforcement flipped on in Plan E.
  • SHA-pin every action. All uses: refs across ALL workflows (new and existing) pin to a full 40-char commit SHA with a trailing # vX.Y.Z version comment. No @v5/@main tag refs may remain after Task 5.
  • harden-runner is Linux-only. step-security/harden-runner runs only on Ubuntu runners. In any matrix that includes macOS/Windows (i.e. release.yml), guard it with if: runner.os == 'Linux'.
  • egress-policy: audit for every harden-runner step (never block in this plan — multi-arch buildx/QEMU/gha-cache make many egress calls).
  • Commit identity: the repo's local git config already uses Jannis Braun <151788261+TheZwiss@users.noreply.github.com> — use a plain git commit. NEVER override author/committer email with -c user.email=..., and never commit as alxtrading94@gmail.com.
  • No new runtime dependencies. This plan touches only .github/ and docs; it must not modify package.json dependency lists or any application/runtime code.
  • Node 20 / pnpm 10.34.3 are the project's pinned toolchain — any workflow that installs deps mirrors ci.yml (pnpm/action-setup @ 10.34.3, actions/setup-node node 20).
  • Branch: all work lands on security-scanning-hardening (already checked out).
  • Action versions: the YAML below uses each action's current major tag. If an action's latest major differs at implementation time, check its README and adjust the tag — then Task 5 pins whatever tag you used to its SHA. A wrong tag surfaces as an actionlint error or a red PR check; fix and re-run.

Task 1: Dependabot config + local validators

Files:

  • Create: .github/dependabot.yml

Interfaces:

  • Consumes: nothing (first task).

  • Produces: .github/dependabot.yml — the Dependabot v2 config later documented by Task 6. No code symbols.

  • Step 1: Install the local validators

actionlint validates workflow YAML; pinact will SHA-pin actions in Task 5. On the macOS dev host:

Run:

brew install actionlint pinact
actionlint --version && pinact --version

Expected: both print a version. (Fallbacks if Homebrew lacks them: go install github.com/rhysd/actionlint/cmd/actionlint@latest and go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest, or run actionlint via Docker docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color.)

  • Step 2: Write .github/dependabot.yml
# Dependabot keeps dependencies and CI actions patched. Three ecosystems:
#   - npm      → the pnpm workspace (Dependabot reads pnpm-lock.yaml v9)
#   - github-actions → action version bumps (feeds the SHA-pin comments)
#   - docker   → the Dockerfile base image (FROM node:20-slim)
#
# NOTE (intentional): there is NO docker entry for docker-compose.yml. It sits
# at the same "/" directory (a second docker entry would collide on
# ecosystem+directory), and Dependabot's docker ecosystem parses Dockerfiles,
# not `image:` refs in compose. The pinned caddy / livekit-server compose images
# are updated MANUALLY — see the maintainer checklist in
# docs/systems/security-scanning.md.
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
    open-pull-requests-limit: 10
    groups:
      # One grouped PR for routine minor/patch bumps to cut PR noise.
      npm-minor-patch:
        update-types:
          - minor
          - patch
    ignore:
      # uiohook-napi is pinned by an exact-version pnpm patch
      # (patches/uiohook-napi@1.5.5.patch). A bump makes the patch path stop
      # matching, breaking `pnpm install --frozen-lockfile` in CI and both
      # Docker stages until the patch is regenerated. Bump it by hand.
      - dependency-name: uiohook-napi

  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly
    groups:
      github-actions:
        patterns:
          - "*"

  - package-ecosystem: docker
    directory: /
    schedule:
      interval: weekly
  • Step 3: Validate YAML syntax

Run:

python3 -c "import yaml,sys; yaml.safe_load(open('.github/dependabot.yml')); print('dependabot.yml: valid YAML')"

Expected: dependabot.yml: valid YAML (no traceback). (The full schema is validated by GitHub after push — Task 7 confirms it in the repo's Insights → Dependency graph → Dependabot.)

  • Step 4: Commit
git add .github/dependabot.yml
git commit -m "ci(security): add Dependabot config (npm + actions + docker)"

Task 2: security.yml — secret, dependency, IaC & license scanning (report-only)

Files:

  • Create: .github/workflows/security.yml

Interfaces:

  • Consumes: pnpm-lock.yaml (OSV lockfile scan), repo tree (gitleaks history, Trivy config/license).

  • Produces: workflow Security with jobs gitleaks, osv-scanner, trivy-config, trivy-license; each uploads a SARIF category (gitleaks, osv-scanner, trivy-config, trivy-license). Task 6 documents these; Task 5 pins their actions.

  • Step 1: Write .github/workflows/security.yml

name: Security

# Report-only in this plan: every scanner is non-blocking and uploads SARIF to
# the Security tab. Enforcement (fail on fixable HIGH/CRITICAL, block on secrets)
# is flipped on in Plan E after the remediation pass.

on:
  pull_request:
  push:
    branches: [main]
  schedule:
    - cron: '32 5 * * 1' # weekly Monday 05:32 UTC

permissions:
  contents: read

concurrency:
  group: security-${{ github.ref }}
  cancel-in-progress: true

jobs:
  gitleaks:
    name: Secret scan (gitleaks)
    runs-on: ubuntu-latest
    steps:
      - name: Harden the runner
        uses: step-security/harden-runner@v2
        with:
          egress-policy: audit
      - name: Checkout (full history)
        uses: actions/checkout@v5
        with:
          fetch-depth: 0 # gitleaks scans the whole git history, not just the diff
      - name: Run gitleaks
        uses: gitleaks/gitleaks-action@v2
        continue-on-error: true # report-only; enforcement flipped on in Plan E
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  osv-scanner:
    name: Dependency scan (OSV-Scanner)
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write # upload SARIF to code scanning
    steps:
      - name: Harden the runner
        uses: step-security/harden-runner@v2
        with:
          egress-policy: audit
      - name: Checkout
        uses: actions/checkout@v5
      - name: Run OSV-Scanner
        uses: google/osv-scanner-action@v2
        continue-on-error: true # report-only; enforcement flipped on in Plan E
        with:
          scan-args: |-
            --lockfile=./pnpm-lock.yaml
            --format=sarif
            --output=osv-results.sarif
      - name: Upload OSV SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: osv-results.sarif
          category: osv-scanner

  trivy-config:
    name: IaC/config scan (Trivy)
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - name: Harden the runner
        uses: step-security/harden-runner@v2
        with:
          egress-policy: audit
      - name: Checkout
        uses: actions/checkout@v5
      - name: Trivy config scan (Dockerfile + docker-compose)
        uses: aquasecurity/trivy-action@0.28.0
        continue-on-error: true # report-only; enforcement flipped on in Plan E
        with:
          scan-type: config
          scan-ref: .
          format: sarif
          output: trivy-config.sarif
      - name: Upload Trivy config SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-config.sarif
          category: trivy-config

  trivy-license:
    name: License compliance scan (Trivy)
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - name: Harden the runner
        uses: step-security/harden-runner@v2
        with:
          egress-policy: audit
      - name: Checkout
        uses: actions/checkout@v5
      - name: Trivy license scan
        uses: aquasecurity/trivy-action@0.28.0
        continue-on-error: true # report-only; enforcement flipped on in Plan E
        with:
          scan-type: fs
          scan-ref: .
          scanners: license
          format: sarif
          output: trivy-license.sarif
      - name: Upload Trivy license SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-license.sarif
          category: trivy-license
  • Step 2: Validate with actionlint

Run:

actionlint .github/workflows/security.yml

Expected: no output (exit 0). If actionlint flags an unknown input for an action, check that action's README and correct it. (Note: actionlint does not fetch action inputs, so most such errors are shellcheck/expression issues — fix those.)

  • Step 3: Commit
git add .github/workflows/security.yml
git commit -m "ci(security): add report-only security scan workflow (gitleaks, OSV, Trivy)"

Task 3: codeql.yml — CodeQL SAST (report-only)

Files:

  • Create: .github/workflows/codeql.yml

Interfaces:

  • Consumes: repo TypeScript/JavaScript source (analyzed with build-mode: none).

  • Produces: workflow CodeQL with job analyze, category /language:javascript-typescript. Findings land in the Security tab. Task 6 documents it; Task 5 pins its actions.

  • Step 1: Write .github/workflows/codeql.yml

name: CodeQL

# Static application security testing for all TS/JS. Uses build-mode: none — no
# compile needed, which sidesteps the monorepo/native-module build entirely.
# Default (code-scanning) query suite; security-extended is deferred (triage tax).
# CodeQL uploads alerts to the Security tab but does NOT fail the PR by itself —
# blocking is a repo setting (code-scanning merge protection), documented in the
# maintainer checklist in docs/systems/security-scanning.md.

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '27 3 * * 1' # weekly Monday 03:27 UTC

permissions:
  contents: read

concurrency:
  group: codeql-${{ github.ref }}
  cancel-in-progress: true

jobs:
  analyze:
    name: Analyze (javascript-typescript)
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write # upload SARIF to code scanning
      actions: read
    steps:
      - name: Harden the runner
        uses: step-security/harden-runner@v2
        with:
          egress-policy: audit
      - name: Checkout
        uses: actions/checkout@v5
      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: javascript-typescript
          build-mode: none
      - name: Perform CodeQL analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:javascript-typescript"
  • Step 2: Validate with actionlint

Run:

actionlint .github/workflows/codeql.yml

Expected: no output (exit 0).

  • Step 3: Commit
git add .github/workflows/codeql.yml
git commit -m "ci(security): add CodeQL SAST workflow (javascript-typescript)"

Task 4: scorecard.yml — OpenSSF Scorecard (report-only)

Files:

  • Create: .github/workflows/scorecard.yml

Interfaces:

  • Consumes: the whole repo + workflow metadata (Scorecard evaluates repo posture).

  • Produces: workflow OpenSSF Scorecard with job analysis; publishes results (feeds the public badge added in Plan E) and uploads SARIF. Task 5 pins its actions.

  • Step 1: Write .github/workflows/scorecard.yml

name: OpenSSF Scorecard

# Scores the repo's security posture (branch protection, pinned deps, token
# permissions, etc.) and publishes to the OpenSSF public API so a badge can be
# shown (badge is added in Plan E). REQUIRES the canonical repo to be PUBLIC —
# see the maintainer checklist in docs/systems/security-scanning.md.

on:
  branch_protection_rule:
  schedule:
    - cron: '18 4 * * 2' # weekly Tuesday 04:18 UTC
  push:
    branches: [main]

permissions: read-all

jobs:
  analysis:
    name: Scorecard analysis
    runs-on: ubuntu-latest
    permissions:
      security-events: write # upload SARIF
      id-token: write        # publish_results OIDC attestation
    steps:
      - name: Harden the runner
        uses: step-security/harden-runner@v2
        with:
          egress-policy: audit
      - name: Checkout
        uses: actions/checkout@v5
        with:
          persist-credentials: false
      - name: Run Scorecard
        uses: ossf/scorecard-action@v2
        with:
          results_file: results.sarif
          results_format: sarif
          publish_results: true
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: SARIF file
          path: results.sarif
          retention-days: 5
      - name: Upload SARIF to code scanning
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif
  • Step 2: Validate with actionlint

Run:

actionlint .github/workflows/scorecard.yml

Expected: no output (exit 0).

  • Step 3: Commit
git add .github/workflows/scorecard.yml
git commit -m "ci(security): add OpenSSF Scorecard workflow"

Task 5: Harden existing workflows + SHA-pin every action

Files:

  • Modify: .github/workflows/ci.yml (add harden-runner step)
  • Modify: .github/workflows/release.yml (add Linux-guarded harden-runner step)
  • Modify: .github/workflows/security.yml, codeql.yml, scorecard.yml, ci.yml, release.yml, cla.yml, deploy-pages.yml, docker-publish.yml (SHA-pin all uses:)

Interfaces:

  • Consumes: all workflow files from Tasks 2-4 plus the four pre-existing ones.

  • Produces: every uses: pinned to @<40-char-sha> # vX.Y.Z; harden-runner (audit) on the two build workflows. No code symbols.

  • Step 1: Add harden-runner to ci.yml

In .github/workflows/ci.yml, insert as the FIRST step of the build-and-test job (before Checkout):

      - name: Harden the runner
        uses: step-security/harden-runner@v2
        with:
          egress-policy: audit
  • Step 2: Add Linux-guarded harden-runner to release.yml

In .github/workflows/release.yml, insert as the FIRST step of the build matrix job (before Checkout). It MUST be guarded — the matrix includes macOS and Windows, where harden-runner does not run:

      - name: Harden the runner
        if: runner.os == 'Linux'
        uses: step-security/harden-runner@v2
        with:
          egress-policy: audit
  • Step 3: SHA-pin every action across all workflows

Run pinact from the repo root — it rewrites each uses: owner/repo@vX to uses: owner/repo@<sha> # vX in place:

pinact run

Manual fallback (if pinact is unavailable) — resolve each tag to its commit SHA with gh and edit by hand. repos/{repo}/commits/{ref} dereferences both lightweight and annotated tags to the commit:

# Example for one action; repeat for every distinct uses: ref.
gh api repos/actions/checkout/commits/v5 --jq '.sha'
# → paste as:  uses: actions/checkout@<sha> # v5
  • Step 4: Verify no unpinned action refs remain

Run (flags any uses: ref NOT pinned to a 40-hex-char SHA — catches both @v5 and non-v semver tags like Trivy's @0.28.0):

grep -rnE 'uses: +[^ ]+@' .github/workflows/ | grep -vE '@[0-9a-f]{40}' && echo "UNPINNED REFS FOUND (fix above)" || echo "All actions pinned to SHA"

Expected: All actions pinned to SHA (the second grep exits non-zero when nothing is unpinned, so the || branch prints). A properly pinned line contains @<40-hex> # vX.Y.Z and is filtered out; any surviving line is an unpinned ref to fix.

  • Step 5: Re-validate all workflows

Run:

actionlint

Expected: no output (exit 0) — actionlint scans every file in .github/workflows/.

  • Step 6: Commit
git add .github/workflows/
git commit -m "ci(security): SHA-pin all actions and add harden-runner (audit)"

Task 6: Document the pipeline

Files:

  • Create: docs/systems/security-scanning.md
  • Modify: CLAUDE.md (add a subsystem-table row)

Interfaces:

  • Consumes: the workflows/config from Tasks 1-5 (documents them).

  • Produces: the security-scanning.md spec + maintainer checklist referenced by every workflow comment; a CLAUDE.md table row. No code symbols.

  • Step 1: Write docs/systems/security-scanning.md

# Security Scanning & Supply-Chain Assurance

Automated, continuous scanning wired into GitHub Actions. This document is the
reference for what runs, where results go, and the one-time settings a maintainer
must enable. **Current state: report-only** — scanners surface findings in the
Security tab but do not block merges yet. Enforcement (blocking) is turned on in a
later change once the remediation pass has cleared the backlog.

## Workflows

| File | Purpose | Trigger | Result |
|------|---------|---------|--------|
| `.github/dependabot.yml` | Dependency + action + base-image update PRs | weekly | PRs |
| `.github/workflows/codeql.yml` | CodeQL SAST (`javascript-typescript`, build-mode none) | PR + push main + weekly | Security tab |
| `.github/workflows/security.yml` | gitleaks (secrets, full history), OSV-Scanner (deps), Trivy config (IaC), Trivy license | PR + push main + weekly | Security tab |
| `.github/workflows/scorecard.yml` | OpenSSF Scorecard (repo posture) | push main + weekly | Security tab + public badge |

## Tiered policy (target, enforced in a later change)

- **Always block:** gitleaks secret hit; OSV/Trivy fixable HIGH/CRITICAL; Trivy
  disallowed license.
- **Advisory (SARIF → Security tab):** CodeQL alerts; OSV/Trivy unfixable or
  medium/low; Scorecard.

Code-level gates (OSV, Trivy, gitleaks) block via workflow exit codes. CodeQL
merge-blocking, Dependabot alerts, and native secret-scanning are GitHub *settings*
— see the checklist below.

## Supply-chain hardening

- Every action is pinned to a full commit SHA (`# vX.Y.Z` comment) — resists
  tag-move attacks and satisfies Scorecard's Pinned-Dependencies check.
- `step-security/harden-runner` (egress-policy `audit`) on Linux jobs.
- Least-privilege `permissions:` per workflow/job.
- SBOM + SLSA provenance are attached to the published container image (added with
  the image-scan work).

## Maintainer checklist (one-time GitHub settings — NOT code)

- [ ] Repository must be **public** (required for the Scorecard badge/publish and
      the CodeQL free tier).
- [ ] Settings → Code security: enable **Dependabot alerts** and **Dependabot
      security updates**.
- [ ] Settings → Code security: enable **Secret scanning** + **Push protection**.
- [ ] Settings → Code security: enable **CodeQL / code-scanning merge protection**
      so high-severity alerts block PRs (the code-level gates do the rest).
- [ ] Branch protection on `main`: require the CI + security status checks to pass.
- [ ] **Manual image bumps:** Dependabot does not track `docker-compose.yml`
      `image:` pins — update `caddy` and `livekit/livekit-server` by hand when new
      releases ship. (Renovate, which parses compose, is an optional future
      alternative.)
  • Step 2: Add the CLAUDE.md subsystem-table row

In CLAUDE.md, inside the "Subsystem Documentation" table (the block of | File | Contents | Read when... | rows), add:

| [security-scanning.md](docs/systems/security-scanning.md) | CI security pipeline: Dependabot, CodeQL SAST, gitleaks, OSV-Scanner, Trivy (config/image/license), OpenSSF Scorecard, SHA-pinning, harden-runner, tiered enforcement policy, maintainer settings checklist | Any CI security work, adding/changing scanners, enabling enforcement, supply-chain hardening |
  • Step 3: Verify the doc links resolve

Run:

test -f docs/systems/security-scanning.md && grep -q 'security-scanning.md' CLAUDE.md && echo "doc + CLAUDE.md row present"

Expected: doc + CLAUDE.md row present.

  • Step 4: Commit
git add docs/systems/security-scanning.md CLAUDE.md
git commit -m "docs(security): document the scanning pipeline + maintainer checklist"

Task 7: Open the PR and verify the pipeline runs

Files: none (verification only).

Interfaces:

  • Consumes: everything from Tasks 1-6, pushed to GitHub.

  • Produces: a PR with all scanners running green/advisory — the acceptance gate for Plan A.

  • Step 1: Push the branch

Run:

git push -u origin security-scanning-hardening

Expected: branch pushed; GitHub prints a PR-create URL.

  • Step 2: Open a PR

Run:

gh pr create --fill --base main --head security-scanning-hardening \
  --title "Security scanning pipeline (report-only)" \
  --body "Adds Dependabot, CodeQL, gitleaks, OSV-Scanner, Trivy (config/license), OpenSSF Scorecard, SHA-pinned actions, and harden-runner. All scanners are report-only; enforcement is flipped on in a later change. See docs/systems/security-scanning.md."

Expected: prints the PR URL.

  • Step 3: Watch the checks

Run:

gh pr checks --watch

Expected: CI / Build & test passes; CodeQL, Security (gitleaks/osv/trivy jobs), and OpenSSF Scorecard all complete. Because every scanner is continue-on-error/advisory, no scanner may report a failing (red) required check — a scanner surfacing findings is fine, but the job itself should not fail the PR. If a job fails for a non-finding reason (bad action input, missing permission), fix the workflow and push.

  • Step 4: Confirm SARIF + Dependabot registration

Verify in the GitHub UI (or note as maintainer follow-up if Actions/security features aren't enabled yet):

  • Security → Code scanning: alerts appear under categories codeql, osv-scanner, trivy-config, trivy-license, and Scorecard.
  • Insights → Dependency graph → Dependabot: the three ecosystems (npm, github-actions, docker) are listed as configured.

Run (CLI cross-check of code-scanning analyses, if the repo is public with Actions enabled):

gh api repos/:owner/:repo/code-scanning/analyses --jq '[.[].category] | unique' 2>/dev/null || echo "code-scanning API not available yet (enable in Settings)"

Expected: a list including the scanner categories, or the fallback message (then it's a maintainer-settings follow-up, not a plan defect).

  • Step 5: Record verification outcome

No commit. Note in the PR description (or a comment) which checks passed and any settings follow-ups (from the Task 6 maintainer checklist) still pending. Plan A is complete when the PR is green with all four scanners running advisory.


Self-Review Notes

  • Spec coverage (WS1 + supply-chain): Dependabot (Task 1) ✓; CodeQL (Task 3) ✓; gitleaks + OSV + Trivy config + Trivy license (Task 2) ✓; Scorecard (Task 4) ✓; SHA-pinning + harden-runner (audit) + least-priv permissions across all workflows (Task 5) ✓; docs + maintainer checklist + CLAUDE.md row (Task 6) ✓; report-only sequencing honored throughout (Global Constraints + per-step comments) ✓.
  • Deferred by design (other plans, not gaps): container image scan + SBOM/provenance + docker-publish restructure → Plan B; helmet/CSP/CORS + DAST → Plan C; Electron hardening → Plan D; remediation of findings + enforcement flip + README badges → Plan E. Task 5's SHA-pin sweep does include docker-publish.yml (harmless; Plan B re-pins as it restructures).
  • Enforcement stays OFF here — every scanner is continue-on-error/advisory; no fail-on/severity gate is set in this plan. The maintainer-settings toggles (CodeQL merge protection, push protection, branch protection) are documented, not enabled in code.