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"]`).