From a6caa79bf4f393c02601fbf2f6df246eae0b1558 Mon Sep 17 00:00:00 2001 From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Date: Sun, 1 Nov 2026 15:24:29 +0800 Subject: [PATCH] ci(codeql): separate production scans from test fixtures (#8450) --- .github/codeql/production.yml | 15 ++ .github/workflows/codeql.yml | 61 ++++++++ docs/testing.md | 30 ++++ scripts/ci/plan.mjs | 5 +- .../boundary/filesystem/ciChangeScope.test.ts | 8 ++ .../boundary/filesystem/ciCodeqlScope.test.ts | 130 ++++++++++++++++++ 6 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 .github/codeql/production.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 tests/boundary/filesystem/ciCodeqlScope.test.ts diff --git a/.github/codeql/production.yml b/.github/codeql/production.yml new file mode 100644 index 0000000000..2fbd5e5171 --- /dev/null +++ b/.github/codeql/production.yml @@ -0,0 +1,15 @@ +name: REA production code + +# Match the existing default-setup scan's local-input threat model. +threat-models: [local] + +# Keep the default query suite and all production roots, including bridge/, +# install.sh, runtime scripts, release controllers, and GitHub Actions. +# Exclude test inputs at extraction time so they cannot taint production callers. +paths-ignore: + - tests + - scripts/fixtures + - "**/*.test.*" + - "**/*.fixture.*" + - "**/test_*.py" + - "**/*_test.py" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..e2e5bb47bc --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,61 @@ +name: CodeQL production + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "37 5 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: codeql-production-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + analyze: + # A repository administrator must disable default setup before enabling + # this workflow; default setup rejects advanced CodeQL result uploads. + if: vars.REA_CODEQL_ADVANCED == 'true' + name: Analyze production (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + security-events: write + strategy: + fail-fast: false + max-parallel: 1 + matrix: + language: [javascript-typescript, python, actions] + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - name: Initialize production analysis + uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4 + with: + languages: ${{ matrix.language }} + build-mode: none + config-file: .github/codeql/production.yml + threads: 2 + ram: 4096 + - name: Analyze production code + uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4 + with: + category: /language:${{ matrix.language }} + output: ${{ runner.temp }}/codeql-production-results + threads: 2 + ram: 4096 + - name: Retain complete analysis evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: codeql-production-${{ matrix.language }} + path: ${{ runner.temp }}/codeql-production-results + if-no-files-found: error + retention-days: 7 diff --git a/docs/testing.md b/docs/testing.md index bce613330e..671688f133 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -365,6 +365,36 @@ Do not report the warm result as a cold-suite improvement, and do not enable the cache in coverage or benchmark CI without first showing that its instrumentation remains equivalent. +## CodeQL production scope + +[The production configuration](../.github/codeql/production.yml) excludes +`tests/`, `scripts/fixtures/`, `*.test.*`, `*.fixture.*`, `test_*.py`, and `*_test.py` inputs +during extraction. It retains the default queries and the existing `local` +threat model. Production source, bridges, runtime scripts, installation and +release controllers, and Actions remain in scope for the configured languages. +Verification scripts remain included unless they match an explicit test path; +excluding the entire `scripts/` directory would hide shipped entrypoints. +These exclusions do not change which behavioral tests CI executes. CodeQL's +JavaScript/TypeScript, Python, and Actions lanes do not add shell, Java, or Swift +analysis; their existing checks remain responsible for those languages. + +[The production workflow](../.github/workflows/codeql.yml) is inactive until a +repository administrator disables CodeQL **default setup** and sets the Actions +repository variable `REA_CODEQL_ADVANCED` to `true`. Disable default setup first: +it rejects advanced CodeQL uploads. Then enable the variable and manually run +**CodeQL production** on `main`; verify all three analyses complete before +relying on the new scope. While the variable is unset, the existing default +setup continues scanning. Adding the configuration file alone does not change +default setup in this user-owned repository. + +Each analysis uses two threads and a 4096 MiB CodeQL memory budget; language +jobs run one at a time. Complete SARIF reports are retained for seven days. +For a scope comparison, use the same source SHA, CodeQL bundle, default query +suite, and `local` threat model on both runs. Change only `paths-ignore`, inspect +the extracted-file inventory and diagnostics, and retain both complete reports. +Alert counts do not establish a false-positive rate or a speedup. Report elapsed +time, CPU time, and process-family memory with their measurement limits. + ## Choose verification for the change `test:local` is fast source-level feedback. For the strongest practical diff --git a/scripts/ci/plan.mjs b/scripts/ci/plan.mjs index 99be6414d7..892f83268f 100644 --- a/scripts/ci/plan.mjs +++ b/scripts/ci/plan.mjs @@ -12,6 +12,7 @@ import { planTestSelection } from "./test-selection.mjs"; // Actionlint validates workflow syntax only. The semantic suite asserts these // workflows' release, publication and self-hosted runner safeguards. const GUARDED_WORKFLOWS = new Set([ + ".github/workflows/codeql.yml", ".github/workflows/pr-title.yml", ".github/workflows/pr-readiness.yml", ".github/workflows/ci-full-pr.yml", @@ -275,10 +276,12 @@ if (tier === "reused") { for (const owner of owners) selected[owner] = true; if ( file.startsWith(".github/workflows/") || + file.startsWith(".github/codeql/") || file === ".github/actionlint.yaml" ) { selected.workflows = true; - if (GUARDED_WORKFLOWS.has(file)) selected.code = true; + if (GUARDED_WORKFLOWS.has(file) || file.startsWith(".github/codeql/")) + selected.code = true; if (file !== ".github/workflows/ci.yml") continue; } // The Notes example is also the source-owned agent evaluation fixture. diff --git a/tests/boundary/filesystem/ciChangeScope.test.ts b/tests/boundary/filesystem/ciChangeScope.test.ts index 1c5b9908a5..03b936b5d0 100644 --- a/tests/boundary/filesystem/ciChangeScope.test.ts +++ b/tests/boundary/filesystem/ciChangeScope.test.ts @@ -1396,6 +1396,14 @@ it.each([ ".github/workflows/pr-readiness.yml", { workflows: true, code: true, package: false, website: false }, ], + [ + ".github/workflows/codeql.yml", + { workflows: true, code: true, runtime: false, package: false }, + ], + [ + ".github/codeql/production.yml", + { workflows: true, code: true, runtime: false, package: false }, + ], [ ".github/workflows/real-ghidra-windows.yml", { workflows: true, code: true, package: false, website: false }, diff --git a/tests/boundary/filesystem/ciCodeqlScope.test.ts b/tests/boundary/filesystem/ciCodeqlScope.test.ts new file mode 100644 index 0000000000..4813412d49 --- /dev/null +++ b/tests/boundary/filesystem/ciCodeqlScope.test.ts @@ -0,0 +1,130 @@ +import { readFile } from "node:fs/promises"; +import { matchesGlob } from "node:path"; +import { expect, it } from "vitest"; +import { parse } from "yaml"; +import { z } from "zod"; + +const github = new URL("../../../.github/", import.meta.url); +const configuration = z + .strictObject({ + name: z.string(), + "threat-models": z.array(z.string()), + "paths-ignore": z.array(z.string()), + }) + .parse( + parse(await readFile(new URL("codeql/production.yml", github), "utf8")), + ); + +const excluded = (path: string): boolean => + configuration["paths-ignore"].some( + (pattern) => + path === pattern || + path.startsWith(`${pattern}/`) || + matchesGlob(path, pattern), + ); + +it.each([ + "tests/fixtures/fakeHopper.mjs", + "tests/conformance/ghidra/riscv-checks.mjs", + "tests/support/analysis-efficiency/resource-run.py", + "scripts/fixtures/http3-lab.py", + "scripts/fixtures/managed/pe.mjs", + "src/process/ExecFileOutput.test.ts", + "src/process/ProcessOwnership.fixture.ts", + "scripts/lib/http3-verifier-lifecycle.test.mjs", + "bridge/pwndbg/test_core_context.py", + "bridge/example_test.py", +])("excludes test-only input %s", (path) => { + expect(excluded(path)).toBe(true); +}); + +it.each([ + "src/process/ExecFileOutput.ts", + "src/filesystem/RegularFile.ts", + "src/application/ClientConfigurationPublication.ts", + "bridge/pwndbg/launch.py", + "bridge/pwntools/layout_impl.py", + "bridge/native/rea_lldb_tracer.py", + "bridge/ghidra/ReaGhidraBridge.java", + "install.sh", + "scripts/rea.mjs", + "scripts/prepack.mjs", + "scripts/hopper-demo-x11.py", + "scripts/ci/release-recovery.mjs", + "scripts/verify-package-install.mjs", + "scripts/verify-package-update.mjs", + "scripts/verify-windows-native-artifact.mjs", + ".github/workflows/release.yml", + ".github/actions/publish-mcp/action.yml", +])("retains production or release input %s", (path) => { + expect(excluded(path)).toBe(false); +}); + +it("retains all packaged entrypoints and the existing local-input threat model", async () => { + const manifest = z + .object({ files: z.array(z.string()) }) + .parse( + JSON.parse( + await readFile( + new URL("../../../package.json", import.meta.url), + "utf8", + ), + ), + ); + expect(manifest.files.filter(excluded)).toEqual([]); + expect(configuration["threat-models"]).toEqual(["local"]); +}); + +it("uses the scope in a bounded workflow gated on administrator activation", async () => { + const workflow = z + .object({ + permissions: z.object({ contents: z.literal("read") }), + jobs: z.object({ + analyze: z.object({ + if: z.string(), + permissions: z.object({ + contents: z.literal("read"), + "security-events": z.literal("write"), + }), + strategy: z.object({ + "max-parallel": z.literal(1), + matrix: z.object({ language: z.array(z.string()) }), + }), + steps: z.array( + z.object({ + uses: z.string(), + with: z.record(z.string(), z.unknown()).optional(), + }), + ), + }), + }), + }) + .parse( + parse(await readFile(new URL("workflows/codeql.yml", github), "utf8")), + ); + const job = workflow.jobs.analyze; + expect(job.if).toBe("vars.REA_CODEQL_ADVANCED == 'true'"); + expect(job.strategy.matrix.language).toEqual([ + "javascript-typescript", + "python", + "actions", + ]); + const init = job.steps.find((step) => + step.uses.startsWith("github/codeql-action/init@"), + ); + expect(init?.with).toMatchObject({ + "config-file": ".github/codeql/production.yml", + "build-mode": "none", + threads: 2, + ram: 4096, + }); + const analyze = job.steps.find((step) => + step.uses.startsWith("github/codeql-action/analyze@"), + ); + expect(analyze?.with).toMatchObject({ + category: "/language:${{ matrix.language }}", + threads: 2, + ram: 4096, + }); + expect(analyze?.with).not.toHaveProperty("upload"); +});