From 3aafbe222d1773eb28ae4cac096535580accfea4 Mon Sep 17 00:00:00 2001 From: Leonid Svyatov Date: Tue, 11 Aug 2026 19:50:15 +0300 Subject: [PATCH] ci: move code scanning to CodeQL advanced setup --- .github/workflows/codeql.yml | 55 ++++++++++++++++++++++++++++++++++++ AGENTS.md | 4 +-- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..9f11635 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,55 @@ +name: codeql + +# Advanced setup rather than default setup, for two reasons. +# +# Default setup returns a neutral verdict on a pull request that changes no +# analyzable code, because it has no analysis to compare against the one on the +# default branch. The `main` ruleset's code_scanning rule blocks on a neutral +# verdict, so every release, which only ever touches three plugin manifests and +# the changelog, was unmergeable. This workflow runs on every pull request with +# no path filter, so a verdict always arrives. +# +# Default setup also excludes pull requests from forks, and every contribution +# here arrives from one, so R-SEC-09 was open on the path that matters most. + +on: + push: + branches: [main] + pull_request: + schedule: + # Keeps the default branch analyzed as CodeQL's own queries change, which + # is what default setup's weekly schedule did. + - cron: "24 4 * * 1" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + analyze: + name: analyze ${{ matrix.language }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + actions: read + security-events: write + strategy: + fail-fast: false + matrix: + # Default setup reported actions, javascript, javascript-typescript, + # and typescript. The middle identifier is the one that covers both + # JavaScript and TypeScript, so naming all three analyzes the same tree + # three times. + language: ["actions", "javascript-typescript"] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + with: + language: ${{ matrix.language }} + - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + with: + category: "/language:${{ matrix.language }}" diff --git a/AGENTS.md b/AGENTS.md index 33309ac..d0f4d58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,9 +98,9 @@ The public API that R-CHG-02 requires is declared in the README under Versioning R-SEC-05 is met: `git cat-file -t v0.13.0` prints `tag`, and `git tag -v v0.13.0` reports a good SSH signature for the ED25519 key published at `gh api users/svyatov/ssh_signing_keys`. Verification needs `gpg.ssh.allowedSignersFile` pointed at an allowed-signers file naming that key with the tagger's email; without it `git tag -v` fails with a configuration error rather than a bad signature, which reads like an unsigned tag and is not one. The rule now carries both the fetch command and that distinction, so an audit on a fresh checkout can resolve it instead of reporting unknown. -R-SEC-04 is met, and R-SEC-09 is open for fork pull requests. The default branch is guarded by a repository ruleset named `main`, scoped to `~DEFAULT_BRANCH` rather than the literal branch name so renaming the branch cannot unguard it. It requires a pull request, resolved review threads, squash as the only merge method, and the four checks CI reports, and it rejects force pushes, deletion, and non-linear history. It requires no approving review, because R-SEC-12 does not reach a repository with one principal and R-SEC-04 no longer asks for one. Expect `Branch-Protection` in an OpenSSF Scorecard result to read 3 of 10 for that reason: its tiers gate each other, review is tier 2, and status checks are tier 3. That is the intended state, not a regression. Read the ruleset with `gh api repos/svyatov/oss-kit/rulesets`, not with the classic `branches/main/protection` endpoint: this repository has no classic branch protection rule, so that endpoint answers `404 Branch not protected`, which reads like an unguarded branch and is not one. Code scanning runs through CodeQL default setup rather than a workflow file, scanning `actions`, `javascript`, `javascript-typescript`, and `typescript`, so nothing under `.github/workflows/` implements it. GitHub excludes fork pull requests from default setup, while this repository routes every external contribution through a fork. Default setup therefore does not meet R-SEC-09 on that path. +R-SEC-04 is met, and R-SEC-09 is open for fork pull requests. The default branch is guarded by a repository ruleset named `main`, scoped to `~DEFAULT_BRANCH` rather than the literal branch name so renaming the branch cannot unguard it. It requires a pull request, resolved review threads, squash as the only merge method, and the four checks CI reports, and it rejects force pushes, deletion, and non-linear history. It requires no approving review, because R-SEC-12 does not reach a repository with one principal and R-SEC-04 no longer asks for one. Expect `Branch-Protection` in an OpenSSF Scorecard result to read 3 of 10 for that reason: its tiers gate each other, review is tier 2, and status checks are tier 3. That is the intended state, not a regression. Read the ruleset with `gh api repos/svyatov/oss-kit/rulesets`, not with the classic `branches/main/protection` endpoint: this repository has no classic branch protection rule, so that endpoint answers `404 Branch not protected`, which reads like an unguarded branch and is not one. Code scanning runs through CodeQL advanced setup, at `.github/workflows/codeql.yml`, and default setup is off. It used to be the other way round, and two things forced the move. Default setup returns a neutral verdict on a pull request that changes no analyzable code, because it has nothing to compare against the default branch, and the ruleset's `code_scanning` rule blocks on neutral, so a release, which only ever touches three plugin manifests and the changelog, was unmergeable. Default setup also excludes fork pull requests, while this repository routes every external contribution through a fork, so R-SEC-09 was open on the path that matters most. The workflow carries no path filter, which is what makes a verdict always arrive. Its matrix names `actions` and `javascript-typescript`, two languages where default setup reported four: `javascript-typescript` is the identifier covering both JavaScript and TypeScript, so naming all four analyzed the same tree three times. Do not add `paths-ignore` to it, because that reintroduces the neutral verdict this workflow exists to prevent. -The ruleset also carries a `code_scanning` rule requiring CodeQL at `security_alerts_threshold: high_or_higher` and `alerts_threshold: errors`, and a `code_quality` rule at `severity: errors`. These block on what the analysis found, where the `CodeQL` entry in `required_status_checks` only blocks on whether it reported. The `code_quality` rule is a live gap. GitHub now documents `GET /repos/{owner}/{repo}/code-quality/setup`, and the read reports that Code Quality is unavailable for this repository. Remove the rule from `main` before relying on it as a merge gate. +The ruleset also carries a `code_scanning` rule requiring CodeQL at `security_alerts_threshold: high_or_higher` and `alerts_threshold: errors`, which blocks on what the analysis found where a required status check would only block on whether it reported. It used to carry a `code_quality` rule at `severity: errors` beside it, and that rule is gone. `gh api repos/svyatov/oss-kit/code-quality/setup` answers `404 Code quality is not available for this repository`, so the rule named a tool that can never report. `skills/oss-harden/references/github.md` already said not to add it to a repository where Code Quality is off, for exactly that reason, and this repository had done it anyway. Do not restore it unless that endpoint starts answering. A ruleset has no "Do not allow bypassing the above settings" checkbox; the equivalent is its bypass list, and this one is empty. Nothing here exempts the owner from anything. It used to hold `RepositoryRole` `5`, Repository admin, at `pull_request` mode, which existed for one reason: the ruleset required an approving review, nobody can approve their own pull request, and without the exemption every change the sole maintainer opened was unmergeable. Dropping the review requirement removed the reason, so the entry went too, and the ruleset now binds the owner as tightly as anybody else. Confirm it from the create or read response, where `bypass_actors` reads `[]` and `current_user_can_bypass` reads `never`. Do not restore the entry to unblock a merge; a required approval nobody can give, undone by an exemption for the only person it named, is strictly weaker than not requiring it.