From 8397577491c26a37a89bedd7bfba427523215145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Fri, 23 Oct 2026 12:25:33 +0200 Subject: [PATCH] ci: add CodeQL code scanning Nothing in CI looked for security problems in the code itself, and code scanning wasn't set up on the repo. This adds codeql.yaml, modelled on terraform-provider-kosli's, analyze job only: - javascript-typescript and actions, both build-mode none, with the security-extended queries. The actions language matters here: the review workflow runs an LLM with a token in scope. - On PRs and pushes to main, plus a weekly baseline re-scan. - Superseded PR runs are cancelled; runs on main are not, so the baseline always gets its SARIF. - SHA-pinned with version comments, harden-runner first, as everywhere else here. It needs no secrets, so it runs on fork PRs unguarded. codeql-action is pinned to v4.39.0 (the provider's pin), not v4.39.1, which is two days old; the repo's 7-day cooldown applies, and Dependabot's actions group will offer it. The Kosli attestation job and making CodeQL a required check are left for later, as the issue says. Closes #91 --- .github/workflows/codeql.yaml | 71 +++++++++++++++++++++++++++++++++++ CLAUDE.md | 2 +- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/codeql.yaml diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 0000000..d234182 --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,71 @@ +# CodeQL code scanning, modelled on terraform-provider-kosli's codeql.yaml +# (analyze job only; the Kosli attestation is deferred until this repo +# reports to a Kosli flow, see #91). +# +# Scans TypeScript/JavaScript (src/, scripts/, test/) and the GitHub Actions +# workflows themselves. Neither needs a build. Results land under +# Security > Code scanning. +# +# Needs no secrets, so it runs unguarded on fork PRs too. + +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Weekly baseline re-scan, so new queries catch old code. An off-peak, + # off-the-hour minute keeps it out of the top-of-hour queue. + - cron: '23 5 * * 3' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + # Cancel superseded PR runs only. A cancelled run on main uploads no SARIF + # and leaves the code scanning baseline stale. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + security-events: write + # Lets codeql-action/analyze read workflow run details if the repository + # is ever made private. + actions: read + strategy: + fail-fast: false + matrix: + include: + # Both are extracted from source, so no build step is needed. + - language: javascript-typescript + build-mode: none + - language: actions + build-mode: none + steps: + - name: Harden runner + uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Initialize CodeQL + uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + queries: security-extended + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0 + with: + category: "/language:${{ matrix.language }}" diff --git a/CLAUDE.md b/CLAUDE.md index 5b43d95..9d38c9e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ A Model Context Protocol (MCP) server that exposes the Kosli API to LLM clients - **`main` is protected by a ruleset**: `deletion`, `non_fast_forward`, `pull_request`, `required_signatures`. Every commit must be signed, including bot-generated ones — that's why `update-catalog.yml` passes `sign-commits: true` to `peter-evans/create-pull-request` (it commits via the GitHub API, which signs as `github-actions[bot]`). An unsigned commit cannot be merged, no matter how green CI is. - **Actions are pinned to full commit SHAs** with a trailing `# vX.Y.Z` comment, and every job runs `step-security/harden-runner` first. Keep both when adding jobs. Dependabot (`.github/dependabot.yml`) updates the pins weekly and preserves the comment; its PR titles are prefixed `ci:`/`chore:` to satisfy Conventional Commits. -- **Four workflows**: `ci.yml` (build, `typecheck`, test, `smoke` against `dist/`, pack, `mcpb validate`, `smoke` against the unzipped bundle), `release.yml` (tags `v*`), `update-catalog.yml` (weekly catalog regeneration PR), `claude-review.yaml` (automated PR review). +- **Five workflows**: `ci.yml` (build, `typecheck`, test, `smoke` against `dist/`, pack, `mcpb validate`, `smoke` against the unzipped bundle), `release.yml` (tags `v*`), `update-catalog.yml` (weekly catalog regeneration PR), `claude-review.yaml` (automated PR review), `codeql.yaml` (CodeQL `security-extended` scan of TypeScript and the workflows, on PRs, pushes to `main` and weekly; results under Security → Code scanning; advisory, not a required check). - **Fork PRs get no secrets.** The repo is public, so PRs from forks run without repository secrets and with a read-only token. Guard any job needing credentials with `github.event.pull_request.head.repo.full_name == github.repository`, or it will fail on every external contribution. - **`claude-review.yaml`** has three jobs, each guarded against forks first, then keyed on `pull_request.user.login`: Dependabot PRs, `update-catalog` PRs (`github-actions[bot]` on branch `chore/update-catalog`; catalog diff — removals need confirming against the live spec), and everything else. Drafts are skipped until marked ready. Reviews are phased: a full review on open, then on each push a `git patch-id` step compares the PR's own diff with the previous push's — a pure rebase runs no review, Dependabot and catalog PRs get a full review again, and general PRs get a cheaper follow-up scoped to the new commits that rechecks earlier findings and resolves fixed threads. The general prompt points at this file for invariants rather than restating them, which is how it drifted before. Auth is OIDC federation via org-wide `ANTHROPIC_*` variables, not secrets — which is why it also works on Dependabot-triggered runs. The action validates its own workflow file and skips with `"The workflow file must exist and have identical content to the version on the repository's default branch"` when it differs — so changes to this file can't be reviewed by themselves and take effect only after merge (observed on PR #33). - **Two Node floors, deliberately.** The *consumer* floor is `engines.node` (`>=22`) — what the published server needs. The *dev* floor is higher (`>=22.12`, pinned in `.nvmrc` and the CI matrix low end) because vitest pulls Vite 8, which requires it. Don't raise `engines.node` to satisfy a devDependency: it constrains consumers, and nothing under `devDependencies` ships (`files: ["dist"]`).