diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..922a11e --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,103 @@ +name: CodeQL + +on: + pull_request: + branches: + - main + push: + branches: + - main + schedule: + - cron: "49 4 * * 1" + workflow_dispatch: + +permissions: + contents: read + security-events: write + +concurrency: + group: codeql-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + analyze-script: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + language: + - actions + - javascript-typescript + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + languages: ${{ matrix.language }} + build-mode: none + + - name: Analyze + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + category: "/language:${{ matrix.language }}" + + analyze-go: + name: Analyze (go) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Initialize CodeQL + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + languages: go + build-mode: autobuild + + - name: Analyze + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + category: /language:go + + analyze-swift: + name: Analyze (swift) + runs-on: macos-26 + timeout-minutes: 45 + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + languages: swift + build-mode: manual + + - name: Build + run: >- + swift build + --package-path macos/CrabfleetMac + --scratch-path "$RUNNER_TEMP/crabfleet-codeql-swift" + --arch arm64 + + - name: Analyze + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + category: /language:swift diff --git a/tests/codeql-workflow.test.ts b/tests/codeql-workflow.test.ts new file mode 100644 index 0000000..a1a5584 --- /dev/null +++ b/tests/codeql-workflow.test.ts @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const workflow = await readFile( + new URL("../.github/workflows/codeql.yml", import.meta.url), + "utf8", +); + +test("CodeQL covers every first-party language with its required build mode", () => { + assert.match(workflow, /- actions\s+- javascript-typescript/); + assert.match(workflow, /languages: go\s+build-mode: autobuild/); + assert.doesNotMatch(workflow, /github\/codeql-action\/autobuild@/); + assert.match(workflow, /languages: swift\s+build-mode: manual/); + assert.match( + workflow, + /swift build\s+--package-path macos\/CrabfleetMac\s+--scratch-path "\$RUNNER_TEMP\/crabfleet-codeql-swift"\s+--arch arm64/, + ); +}); + +test("CodeQL uses pinned actions and least-privilege checkout", () => { + assert.match(workflow, /permissions:\s+contents: read\s+security-events: write/); + assert.doesNotMatch(workflow, /pull_request_target/); + assert.equal( + workflow.match( + /actions\/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7\.0\.1\s+with:\s+persist-credentials: false/g, + )?.length, + 3, + ); + assert.equal( + workflow.match( + /github\/codeql-action\/(?:init|analyze)@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4\.37\.9/g, + )?.length, + 6, + ); + assert.match( + workflow, + /actions\/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7\.0\.0\s+with:\s+go-version-file: go\.mod\s+cache-dependency-path: go\.sum/, + ); + assert.doesNotMatch(workflow, /uses:\s+[^@\s]+@(?![0-9a-f]{40}(?:\s|$))/); +}); + +test("CodeQL analyzes pull requests, main, schedules, and manual runs", () => { + assert.match(workflow, /^on:$/m); + assert.match(workflow, /pull_request:\s+branches:\s+- main/); + assert.match(workflow, /push:\s+branches:\s+- main/); + assert.match(workflow, /schedule:\s+- cron:/); + assert.match(workflow, /workflow_dispatch:/); +});