From 00d700a69273dc66a8fd26899998c1e70fec56f9 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Sun, 23 Aug 2026 18:12:31 +0500 Subject: [PATCH] feat(codeql): isolate Dependabot analysis routing Signed-off-by: rldyourmnd --- .github/workflows/public-codeql.yml | 17 +++++++++-- CHANGELOG.md | 4 +++ catalog/capabilities.yml | 6 +++- docs/01-public-oss-free.md | 7 ++++- docs/03-private-paid-ghas.md | 2 ++ docs/generated/capability-matrix.md | 2 +- docs/generated/workflow-inventory.md | 2 +- .../nddev/security-private-selfhosted.yml | 1 + examples/nddev/security.yml | 1 + examples/private-paid-ghas/security.yml | 1 + examples/public-oss/security.yml | 1 + scripts/check_workflow_contracts.py | 29 +++++++++++++++++++ 12 files changed, 67 insertions(+), 6 deletions(-) diff --git a/.github/workflows/public-codeql.yml b/.github/workflows/public-codeql.yml index 52d4802..d577d94 100644 --- a/.github/workflows/public-codeql.yml +++ b/.github/workflows/public-codeql.yml @@ -50,7 +50,11 @@ on: type: number default: 14 runner: - description: 'Runner label.' + description: 'Runner label for ordinary trusted events.' + type: string + default: 'ubuntu-latest' + dependabot_runner: + description: 'Isolated runner label for pull requests authored by dependabot[bot].' type: string default: 'ubuntu-latest' egress_policy: @@ -63,7 +67,7 @@ permissions: {} jobs: codeql: name: CodeQL (${{ matrix.language }}) - runs-on: ${{ inputs.runner }} + runs-on: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' && inputs.dependabot_runner || inputs.runner }} timeout-minutes: 20 permissions: actions: read # upload-sarif reads this workflow run @@ -79,6 +83,15 @@ jobs: with: egress-policy: ${{ inputs.egress_policy }} + - name: Record CodeQL trust boundary + shell: bash + env: + TRUST_BOUNDARY: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' && 'dependabot-untrusted' || 'ordinary' }} + SELECTED_RUNNER: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' && inputs.dependabot_runner || inputs.runner }} + run: | + printf "CodeQL trust boundary: \`%s\`\n\nRunner: \`%s\`\n" \ + "$TRUST_BOUNDARY" "$SELECTED_RUNNER" >> "$GITHUB_STEP_SUMMARY" + - name: Isolate global Git config shell: bash run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index d9aed94..671a10f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The project follows Semantic Versioning. ## [Unreleased] +- Added explicit Dependabot-safe advanced CodeQL routing. Ordinary private + analysis and Dependabot pull requests can use separate reviewed runner + classes while preserving stable language check identities and a secretless + `pull_request` trust boundary. - Added validated opt-in dependency cache inputs to `python-ci.yml` pip callers and `node-ci.yml` npm/pnpm/Yarn callers. Cache backends must match the package manager and dependency paths cannot be supplied without enabling a cache. diff --git a/catalog/capabilities.yml b/catalog/capabilities.yml index 5d6b49e..830536b 100644 --- a/catalog/capabilities.yml +++ b/catalog/capabilities.yml @@ -25,14 +25,18 @@ capabilities: required_settings: - "Code scanning enabled" - "GitHub Advanced Security enabled (required on private repos)" + - "Dependabot pull requests use the reviewed dependabot_runner trust boundary" risks: - "Paid via GHAS on private repositories; keep upload disabled there" - "Language autobuild can fail on compiled languages without a build spec" + - "Dependabot pull_request events have read-only tokens and no Actions secrets; use pull_request, never pull_request_target, and keep checkout credentials disabled" deprecations: null - last_verified: "2026-07-04" + last_verified: "2026-08-23" sources: - "https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql" - "https://github.com/github/codeql-action" + - "https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-on-actions" + - "https://docs.github.com/en/code-security/reference/code-scanning/troubleshoot-analysis-errors/resource-not-accessible" - id: github-code-quality name: GitHub Code Quality diff --git a/docs/01-public-oss-free.md b/docs/01-public-oss-free.md index c20ce67..083362d 100644 --- a/docs/01-public-oss-free.md +++ b/docs/01-public-oss-free.md @@ -55,10 +55,15 @@ jobs: with: languages: '["python","actions"]' queries: security-and-quality + runner: ubuntu-latest + dependabot_runner: ubuntu-latest ``` Inputs: `languages` (JSON array), `queries` (suite, default -`security-and-quality`), `runner`, `egress_policy`. See +`security-and-quality`), `runner`, `dependabot_runner`, `egress_policy`. Public +callers keep both on `ubuntu-latest`; private GHAS callers may route ordinary +analysis to a trusted private class and Dependabot pull requests to an isolated +untrusted class without changing the `CodeQL ()` check identity. See [06 Security scanning](06-security-scanning.md#codeql) for language coverage and Copilot Autofix. diff --git a/docs/03-private-paid-ghas.md b/docs/03-private-paid-ghas.md index c615a85..8a95e04 100644 --- a/docs/03-private-paid-ghas.md +++ b/docs/03-private-paid-ghas.md @@ -65,6 +65,8 @@ jobs: uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-codeql.yml@ with: languages: '["python","actions"]' + runner: nddev-linux-standard + dependabot_runner: nddev-linux-untrusted zizmor: permissions: contents: read diff --git a/docs/generated/capability-matrix.md b/docs/generated/capability-matrix.md index 52a094d..23993c3 100644 --- a/docs/generated/capability-matrix.md +++ b/docs/generated/capability-matrix.md @@ -82,4 +82,4 @@ | SLSA build provenance (`slsa-build-provenance`) | supply-chain | ga | free | paid | conditional | `.github/workflows/release-supply-chain.yml` | `-` | --- -Source data verified through: 2026-08-22 +Source data verified through: 2026-08-23 diff --git a/docs/generated/workflow-inventory.md b/docs/generated/workflow-inventory.md index e9be455..5c2654f 100644 --- a/docs/generated/workflow-inventory.md +++ b/docs/generated/workflow-inventory.md @@ -64,4 +64,4 @@ | `.github/workflows/zizmor-sarif.yml` | `zizmor` | ga | --- -Source data verified through: 2026-08-22 +Source data verified through: 2026-08-23 diff --git a/examples/nddev/security-private-selfhosted.yml b/examples/nddev/security-private-selfhosted.yml index fa416fb..d8d4afb 100644 --- a/examples/nddev/security-private-selfhosted.yml +++ b/examples/nddev/security-private-selfhosted.yml @@ -51,6 +51,7 @@ jobs: with: languages: '["python","actions"]' runner: nddev-linux-standard + dependabot_runner: nddev-linux-untrusted security-bundle: permissions: { contents: read } uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/nddev-security-bundle.yml@ diff --git a/examples/nddev/security.yml b/examples/nddev/security.yml index 59e941e..96c9b2e 100644 --- a/examples/nddev/security.yml +++ b/examples/nddev/security.yml @@ -17,6 +17,7 @@ jobs: uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-codeql.yml@ with: runner: ubuntu-latest + dependabot_runner: ubuntu-latest languages: '["python","actions"]' secret-scan: permissions: { contents: read } diff --git a/examples/private-paid-ghas/security.yml b/examples/private-paid-ghas/security.yml index 77048bf..9c5624b 100644 --- a/examples/private-paid-ghas/security.yml +++ b/examples/private-paid-ghas/security.yml @@ -15,6 +15,7 @@ jobs: uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-codeql.yml@ with: runner: ubuntu-latest + dependabot_runner: ubuntu-latest languages: '["python","actions"]' secret-scan: permissions: { contents: read } diff --git a/examples/public-oss/security.yml b/examples/public-oss/security.yml index cab587b..debb6a5 100644 --- a/examples/public-oss/security.yml +++ b/examples/public-oss/security.yml @@ -21,6 +21,7 @@ jobs: uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-codeql.yml@ with: runner: ubuntu-latest + dependabot_runner: ubuntu-latest languages: '["python","actions"]' secret-scan: permissions: { contents: read } diff --git a/scripts/check_workflow_contracts.py b/scripts/check_workflow_contracts.py index ec5e57e..d3ab66a 100644 --- a/scripts/check_workflow_contracts.py +++ b/scripts/check_workflow_contracts.py @@ -48,6 +48,35 @@ def check() -> list[str]: """ workflow_root = workflow_files()[0].parent + codeql = load_yaml(workflow_root / "public-codeql.yml") + codeql_on = get_on(codeql) + codeql_call = codeql_on.get("workflow_call", {}) if isinstance(codeql_on, dict) else {} + codeql_inputs = codeql_call.get("inputs", {}) if isinstance(codeql_call, dict) else {} + dependabot_runner = codeql_inputs.get("dependabot_runner", {}) if isinstance(codeql_inputs, dict) else {} + if not isinstance(dependabot_runner, dict) or dependabot_runner.get("type") != "string" or dependabot_runner.get("default") != "ubuntu-latest": + problems.append( + "public-codeql.yml: dependabot_runner must remain a string with the " + "public-safe ubuntu-latest default" + ) + codeql_job = (codeql.get("jobs", {}) or {}).get("codeql", {}) + expected_codeql_runner = "${{ github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' && inputs.dependabot_runner || inputs.runner }}" + if codeql_job.get("runs-on") != expected_codeql_runner: + problems.append( + "public-codeql.yml: runner selection must explicitly isolate " + "Dependabot-authored pull requests" + ) + boundary_steps = [ + step for step in (codeql_job.get("steps", []) or []) + if isinstance(step, dict) and step.get("name") == "Record CodeQL trust boundary" + ] + if len(boundary_steps) != 1 or (boundary_steps[0].get("env") or {}).get("SELECTED_RUNNER") != expected_codeql_runner: + problems.append( + "public-codeql.yml: selected Dependabot/ordinary runner boundary must " + "be emitted as job evidence" + ) + if "pull_request_target" in (workflow_root / "public-codeql.yml").read_text(encoding="utf-8"): + problems.append("public-codeql.yml: Dependabot analysis must never use pull_request_target") + # The consolidated scanner requires four output paths before it can create # empty fail-safe evidence. Changing the shared script without wiring every # caller broke the first public-product consumer ring at shell expansion,