Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 71 additions & 17 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ concurrency:
# Key on the full ref so each PR/branch has its own group. Do NOT cancel
# in-progress runs: the repo ruleset's code_scanning gate evaluates CodeQL
# against the PR merge commit, which regenerates whenever main moves. Cancelling
# in-flight PR runs (esp. the ~30min swift build) made the gate unsatisfiable
# under merge load — every main move murdered the run before it could report.
# Letting runs finish (and queueing the next) preserves full 5-language required
# coverage and dissolves that deadlock, at the cost of some extra runner minutes.
# in-flight PR runs made the gate unsatisfiable under merge load — every main
# move murdered the run before it could report. Letting runs finish (and
# queueing the next) preserves required coverage and dissolves that deadlock,
# at the cost of some extra runner minutes.
group: codeql-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

Expand Down Expand Up @@ -51,29 +51,59 @@ jobs:
build-mode: none
runner: ubuntu-latest
timeout: 20
# swift/macos-15 leg removed from PR CI: GitHub-hosted macOS minutes
# were exhausting the org Actions limit and queuing indefinitely,
# making the Analyze (swift) check unsatisfiable. Restore
# this entry once macOS Actions budget is sorted.
# swift is NOT in this matrix on purpose. It runs out-of-band in the
# analyze-swift-audit job below (schedule/dispatch only, no upload).
# Gate-parity invariant: the ruleset's code_scanning rule derives its
# "expected" analysis categories from what has been uploaded to main.
# Uploading swift from main-only runs while PRs analyze four languages
# made the gate permanently unsatisfiable — every PR merge stalled on
# "still expecting 1 result from CodeQL" (issue #3285). Only re-add a
# swift matrix entry if it runs on pull_request too (macOS budget).

steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- name: Initialize CodeQL
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
# Default setup rejects advanced SARIF uploads. Keep the migration
# workflow green in analysis-only mode until an administrator
# disables default setup and sets this repository variable to always.
upload: ${{ vars.CODEQL_ADVANCED_UPLOAD || 'never' }}

# Out-of-band swift security audit: weekly + on demand. Scans the generated
# iOS project on a macOS runner but NEVER uploads to code scanning, so the
# swift category stays out of the merge gate's expected set (see the matrix
# comment above). Findings surface in the run's step summary and as a SARIF
# artifact. Cost: one macOS run per week (~30 min) instead of one per PR.
analyze-swift-audit:
name: Analyze (swift audit)
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: macos-15
timeout-minutes: 40
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

# Install build tooling before CodeQL initializes its Swift tracer. On
# Apple Silicon, Homebrew rejects installs invoked through Rosetta.
- name: Install XcodeGen
if: matrix.language == 'swift'
run: brew install xcodegen

- name: Initialize CodeQL
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
languages: swift
build-mode: manual

- name: Generate and build the iOS project
if: matrix.language == 'swift'
env:
DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer
run: |
Expand All @@ -90,10 +120,34 @@ jobs:
ARCHS=arm64 \
build

- name: Perform CodeQL analysis
- name: Perform CodeQL analysis (no upload)
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
# Default setup rejects advanced SARIF uploads. Keep the migration
# workflow green in analysis-only mode until an administrator
# disables default setup and sets this repository variable to always.
upload: ${{ vars.CODEQL_ADVANCED_UPLOAD || 'never' }}
# Hard-coded on purpose — do NOT switch to the repository variable.
# An uploaded swift analysis on main re-poisons the merge gate for
# every four-language PR run (gate-parity invariant above).
upload: never
output: swift-sarif

- name: Summarize swift findings
run: |
set -euo pipefail
SARIF=swift-sarif/swift.sarif
COUNT=$(jq '[.runs[].results[]] | length' "$SARIF")
{
echo "## CodeQL swift audit"
echo
echo "Findings: **$COUNT** (SARIF attached as a run artifact)"
if [ "$COUNT" -gt 0 ]; then
echo
jq -r '.runs[].results[] | "- `\(.ruleId)` \(.locations[0].physicalLocation.artifactLocation.uri // "?"):\(.locations[0].physicalLocation.region.startLine // 0)"' "$SARIF"
fi
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload swift SARIF artifact
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: codeql-swift-sarif
path: swift-sarif
retention-days: 30
105 changes: 69 additions & 36 deletions docs/codeql.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,71 @@
# CodeQL configuration

The advanced workflow is configured to scan GitHub Actions,
JavaScript/TypeScript, Python, Rust, and Swift. It is required because the
native iOS project is generated from
`apps/ios/CovenCave/project.yml`; default setup cannot run `xcodegen` before
Swift autobuild searches for an Xcode project.

## Activating advanced setup

GitHub default setup overrides checked-in CodeQL workflows and rejects their
result uploads. Until cutover, the advanced workflow defaults
`CODEQL_ADVANCED_UPLOAD` to `never`, so it can prove that every language builds
and analyzes without making the migration pull request fail at upload time. A
repository administrator must perform this transition after
`.github/workflows/codeql.yml` reaches `main`:

1. In **Settings > Security > Code security**, disable CodeQL default setup.
2. In **Settings > Secrets and variables > Actions > Variables**, create the
repository variable `CODEQL_ADVANCED_UPLOAD` with value `always`.
3. In **Actions**, enable the **CodeQL Advanced** workflow if GitHub left it
disabled, then run it with **Run workflow** on `main`.
4. Confirm all five `Analyze (...)` jobs succeed and that the latest code
scanning analyses contain these categories:
`/language:actions`, `/language:javascript-typescript`, `/language:python`,
`/language:rust`, and `/language:swift`.
5. Add an active branch ruleset for `main` with **Require code scanning
results** set to tool `CodeQL`, security threshold **High or higher**, and
alerts threshold **None**. Do not remove the existing required CI checks.
6. Open a pull request and confirm merge is blocked until the CodeQL analysis
finishes, and when a new High or Critical security result is reported.

If the advanced workflow cannot upload results, first verify that default setup
is disabled and `CODEQL_ADVANCED_UPLOAD` is `always`. For rollback, set the
variable to `never` before re-enabling default setup so the two configurations
do not compete. Re-enable default setup immediately if the advanced workflow
cannot be made healthy, so the existing four-language security coverage is not
left inactive.
CodeQL runs through the checked-in advanced workflow
(`.github/workflows/codeql.yml`); GitHub default setup is disabled. The
migration and enforcement work is tracked in issue #3285.

## Current state

- **Gated languages (push + PR, ubuntu):** GitHub Actions,
JavaScript/TypeScript, Python, Rust. These upload SARIF (repository variable
`CODEQL_ADVANCED_UPLOAD=always`) and feed the merge gate.
- **Swift (audit-only, weekly + on demand):** the `Analyze (swift audit)` job
builds the generated iOS project (`xcodegen` from
`apps/ios/CovenCave/project.yml`, then `xcodebuild`) on `macos-15` for
`schedule`/`workflow_dispatch` events only, with `upload: never`. Findings
appear in the run's step summary and as a `codeql-swift-sarif` artifact
(30-day retention). Swift needs the advanced workflow because default setup
cannot run `xcodegen` before Swift autobuild looks for an Xcode project.
- **Merge gate:** branch ruleset **CodeQL merge gate** (id 19123333) on `main`
requires code scanning results from CodeQL with security threshold **High or
higher** (alerts threshold **None**), plus the standard required CI checks.
Repository admins can bypass for pull requests; direct pushes cannot bypass.

## The gate-parity invariant (read before touching the matrix)

The ruleset's `code_scanning` rule derives the set of *expected* analysis
categories from what has been uploaded to `main`. A pull request only
satisfies the gate when its merge/head commit has results for **every**
expected category.

Uploading a category from main-only runs while PRs don't produce it makes the
gate **permanently unsatisfiable**: every PR fails with *"Code scanning is
still expecting N results from CodeQL"*, and only admin bypass can merge. This
happened when the swift matrix leg was removed from PR runs (macOS budget)
while its analyses remained on `main` — see issue #3285. Recovery required
deleting the stale swift analyses from `main` via
`DELETE /repos/{owner}/{repo}/code-scanning/analyses/{id}?confirm_delete=true`.

Rules that follow from this:

1. Every category uploaded on `push` to `main` must also be produced by
`pull_request` runs (keep the trigger pair symmetric for gated legs).
2. Audit-only legs (swift) must keep `upload: never` hard-coded.
3. To promote swift to a gated language, add it back to the matrix for both
push **and** PR events — accepting one macOS run per PR — and never
main-only.

The workflow also sets `cancel-in-progress: false`: the gate evaluates the PR
*merge commit*, which regenerates whenever `main` moves, so cancelling
in-flight runs under merge load re-creates the same deadlock from the other
side. Expect the gate to lag `main` by one CodeQL run (~10 min) during rapid
merge trains; re-running the PR's CodeQL workflow (or waiting for the
merge-commit run) clears it.

## Verifying the gate

- List recent evaluations: `gh api repos/{owner}/{repo}/rulesets/rule-suites`
— merges show `result: pass` (or `bypass` with the `code_scanning` rule's
failure detail while a run is still in flight).
- A PR with all analyses uploaded and no open High/Critical alerts merges
without bypass; a PR whose CodeQL run is still executing is blocked with
*"still expecting N results"*.

## Rollback

If the advanced workflow cannot be kept healthy: set
`CODEQL_ADVANCED_UPLOAD=never`, re-enable default setup (Settings > Security >
Code security) so the four ubuntu languages stay covered, and delete or
deactivate the **CodeQL merge gate** ruleset (default setup categories differ,
so the old expected set would block PRs). Never run default setup and advanced
uploads side by side — they compete for the same categories.
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def main() -> None:
Inches(2.0), Inches(2.0)
)
p = open_quote.text_frame.paragraphs[0]
p.text = """
p.text = "\u201C" # Left double quote mark
p.font.name = BRAND_HEADING_FONT
p.font.size = Pt(180)
p.font.bold = True
Expand Down
Loading