diff --git a/.github/workflows/rg-security.yml b/.github/workflows/rg-security.yml index 692ee35..a7e24ff 100644 --- a/.github/workflows/rg-security.yml +++ b/.github/workflows/rg-security.yml @@ -39,10 +39,10 @@ jobs: timeout-minutes: 20 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 + - uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4 with: languages: ${{ inputs.codeql-languages }} - - uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 + - uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4 - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 if: inputs.enable-dependency-review && github.event_name == 'pull_request' with: @@ -55,7 +55,7 @@ jobs: --lockfile=${{ inputs.lockfile }} --format=sarif --output=osv-results.sarif - - uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 + - uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4 if: always() && hashFiles('osv-results.sarif') != '' with: sarif_file: osv-results.sarif diff --git a/test/security-workflow.test.ts b/test/security-workflow.test.ts index 219ce80..8dbb707 100644 --- a/test/security-workflow.test.ts +++ b/test/security-workflow.test.ts @@ -5,7 +5,61 @@ import { describe, expect, test } from "vitest"; const shellSafePublicRunner = ["self-hosted", "linux", "shell-only", "public"]; +type WorkflowStep = { uses?: string }; + +function expectCodeqlPins(rgSecuritySteps: WorkflowStep[]) { + const codeqlActionVersions = rgSecuritySteps + .map((step) => step.uses) + .filter((uses): uses is string => uses?.startsWith("github/codeql-action/") ?? false) + .map((uses) => uses.split("@")[1]); + expect(codeqlActionVersions).toEqual([ + "24c54180a607b1449ed407dd24f251e4e9147c8d", + "24c54180a607b1449ed407dd24f251e4e9147c8d", + "24c54180a607b1449ed407dd24f251e4e9147c8d" + ]); + + const codeqlSteps = rgSecuritySteps.filter((step): step is { uses: string } => + step.uses?.startsWith("github/codeql-action/") ?? false + ); + const codeqlVersions = codeqlSteps.map((step) => step.uses.split("@")[1]); + expect(codeqlVersions).toHaveLength(3); + expect(new Set(codeqlVersions)).toEqual( + new Set([ + "24c54180a607b1449ed407dd24f251e4e9147c8d" + ]) + ); + expect(codeqlSteps.map((step) => step.uses?.split("@")[0])).toEqual([ + "github/codeql-action/init", + "github/codeql-action/analyze", + "github/codeql-action/upload-sarif" + ]); +} + describe("security and reusable workflows", () => { + test.each(["wrong", "missing", "mixed", "tag", "duplicate"])( + "rejects %s CodeQL subaction pins", + (mutation) => { + const workflow = YAML.parse( + fs.readFileSync(path.resolve(".github/workflows/rg-security.yml"), "utf8") + ); + const steps = (workflow.jobs.security.steps as WorkflowStep[]).filter( + (step) => step.uses?.startsWith("github/codeql-action/") + ); + if (mutation === "wrong") { + for (const step of steps) step.uses = step.uses!.replace(/@.*/, "@" + "0".repeat(40)); + } else if (mutation === "missing") { + steps.splice(1, 1); + } else if (mutation === "mixed") { + steps[1].uses = steps[1].uses!.replace(/@.*/, "@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2"); + } else if (mutation === "tag") { + for (const step of steps) step.uses = step.uses!.replace(/@.*/, "@v4.38.3"); + } else { + steps[1].uses = steps[0].uses; + } + expect(() => expectCodeqlPins(steps)).toThrow(); + } + ); + test("keeps security scans on GitHub-hosted runners with Security tab upload", () => { const workflow = YAML.parse( fs.readFileSync(path.resolve(".github/workflows/security.yml"), "utf8") @@ -73,27 +127,7 @@ describe("security and reusable workflows", () => { for (const job of Object.values(rgSecurity.jobs)) { expect(job["runs-on"]).toBe("ubuntu-24.04"); } - const rgSecuritySteps = rgSecurity.jobs.security.steps as Array<{ uses?: string }>; - const codeqlActionVersions = rgSecuritySteps - .map((step) => step.uses) - .filter((uses): uses is string => uses?.startsWith("github/codeql-action/") ?? false) - .map((uses) => uses.split("@")[1]); - expect(codeqlActionVersions).toEqual([ - "2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2", - "2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2", - "2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2" - ]); - - const codeqlSteps = rgSecurity.jobs.security.steps.filter((step: { uses?: string }) => - step.uses?.startsWith("github/codeql-action/") - ); - const codeqlVersions = codeqlSteps.map((step: { uses: string }) => step.uses.split("@")[1]); - expect(codeqlVersions).toHaveLength(3); - expect(new Set(codeqlVersions)).toEqual( - new Set([ - "2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2" - ]) - ); + expectCodeqlPins(rgSecurity.jobs.security.steps as WorkflowStep[]); const rgRelease = YAML.parse( fs.readFileSync(path.resolve(".github/workflows/rg-release.yml"), "utf8")