From 08877de0c157a3d0669b9d59e015ca135e0bd32c Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:14:55 +0530 Subject: [PATCH 1/4] ci: allow trusted Dependabot branches through policy --- .github/workflows/issue-branch-policy.yml | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/issue-branch-policy.yml b/.github/workflows/issue-branch-policy.yml index 3d7929e..50014f2 100644 --- a/.github/workflows/issue-branch-policy.yml +++ b/.github/workflows/issue-branch-policy.yml @@ -122,6 +122,7 @@ jobs: local pull_request_number="$1" local expected_head_sha="$2" local pull_request_json branch_name head_sha + local author_login head_repo_full_name local category issue branch_date issue_json category_lines local issue_categories=() local issue_category @@ -152,6 +153,30 @@ jobs: return 2 fi + author_login="$(jq -r '.user.login // ""' <<< "$pull_request_json")" || { + printf 'Pull request #%s did not provide a readable author.\n' \ + "$pull_request_number" >&2 + return 1 + } + head_repo_full_name="$(jq -r '.head.repo.full_name // ""' <<< "$pull_request_json")" || { + printf 'Pull request #%s did not provide a readable head repository.\n' \ + "$pull_request_number" >&2 + return 1 + } + + # Dependabot branches are generated by GitHub and cannot use the + # issue-backed branch naming convention. Allow only the trusted + # Dependabot bot, and only when its branch lives in this repository; + # all human- or fork-owned branches still require the canonical + # issue-backed format below. + if [[ "$author_login" == "dependabot[bot]" && + "$head_repo_full_name" == "$GITHUB_REPOSITORY" && + "$branch_name" == dependabot/* ]]; then + printf 'Validated Dependabot branch %s for pull request #%s.\n' \ + "$branch_name" "$pull_request_number" + return 0 + fi + if [[ ! "$branch_name" =~ $branch_pattern ]]; then printf 'Pull request #%s: branch name is not canonical.\n' \ "$pull_request_number" >&2 From d4cf3da293cc9606c5c7d67bc839b763bc2794e0 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:39:56 +0530 Subject: [PATCH 2/4] ci: validate current Dependabot head commits --- .github/workflows/issue-branch-policy.yml | 39 +++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/.github/workflows/issue-branch-policy.yml b/.github/workflows/issue-branch-policy.yml index 50014f2..a2b7050 100644 --- a/.github/workflows/issue-branch-policy.yml +++ b/.github/workflows/issue-branch-policy.yml @@ -122,7 +122,8 @@ jobs: local pull_request_number="$1" local expected_head_sha="$2" local pull_request_json branch_name head_sha - local author_login head_repo_full_name + local head_repo_full_name head_commit_json + local head_commit_author_login head_commit_verified head_commit_verification_reason local category issue branch_date issue_json category_lines local issue_categories=() local issue_category @@ -153,23 +154,41 @@ jobs: return 2 fi - author_login="$(jq -r '.user.login // ""' <<< "$pull_request_json")" || { - printf 'Pull request #%s did not provide a readable author.\n' \ + head_repo_full_name="$(jq -r '.head.repo.full_name // ""' <<< "$pull_request_json")" || { + printf 'Pull request #%s did not provide a readable head repository.\n' \ "$pull_request_number" >&2 return 1 } - head_repo_full_name="$(jq -r '.head.repo.full_name // ""' <<< "$pull_request_json")" || { - printf 'Pull request #%s did not provide a readable head repository.\n' \ + + head_commit_json="$(gh api "repos/$GITHUB_REPOSITORY/commits/$head_sha")" || { + printf 'Pull request #%s did not provide a readable head commit.\n' \ + "$pull_request_number" >&2 + return 1 + } + head_commit_author_login="$(jq -r '.author.login // ""' <<< "$head_commit_json")" || { + printf 'Pull request #%s did not provide a readable head commit author.\n' \ + "$pull_request_number" >&2 + return 1 + } + head_commit_verified="$(jq -r '.commit.verification.verified // false' <<< "$head_commit_json")" || { + printf 'Pull request #%s did not provide readable head commit verification state.\n' \ + "$pull_request_number" >&2 + return 1 + } + head_commit_verification_reason="$(jq -r '.commit.verification.reason // ""' <<< "$head_commit_json")" || { + printf 'Pull request #%s did not provide a readable head commit verification reason.\n' \ "$pull_request_number" >&2 return 1 } # Dependabot branches are generated by GitHub and cannot use the - # issue-backed branch naming convention. Allow only the trusted - # Dependabot bot, and only when its branch lives in this repository; - # all human- or fork-owned branches still require the canonical - # issue-backed format below. - if [[ "$author_login" == "dependabot[bot]" && + # issue-backed branch naming convention. The PR author is not a + # trustworthy identity check: a collaborator can push a new head + # commit to an existing Dependabot branch after it is opened. Use + # the current head commit's verified GitHub identity instead. + if [[ "$head_commit_author_login" == "dependabot[bot]" && + "$head_commit_verified" == true && + "$head_commit_verification_reason" == valid && "$head_repo_full_name" == "$GITHUB_REPOSITORY" && "$branch_name" == dependabot/* ]]; then printf 'Validated Dependabot branch %s for pull request #%s.\n' \ From ea9d3c2a66e5c6d427355fabb0bf4372c902024f Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:26:15 +0530 Subject: [PATCH 3/4] test: cover Dependabot head commit policy bypass --- tests/test_issue_branch_policy.py | 83 +++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_issue_branch_policy.py diff --git a/tests/test_issue_branch_policy.py b/tests/test_issue_branch_policy.py new file mode 100644 index 0000000..7c02112 --- /dev/null +++ b/tests/test_issue_branch_policy.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import os +import stat +import subprocess +import tempfile +from pathlib import Path + +import yaml + + +def test_dependabot_policy_rejects_an_unverified_collaborator_head_commit() -> None: + root = Path(__file__).resolve().parents[1] + workflow = yaml.safe_load((root / ".github/workflows/issue-branch-policy.yml").read_text(encoding="utf-8")) + policy_run = workflow["jobs"]["policy"]["steps"][0]["run"] + head_sha = "a" * 40 + + fake_gh = """#!/bin/sh +set -eu +path="" +for argument in "$@"; do + case "$argument" in + repos/*) path="$argument"; break ;; + esac +done +case "$path" in + repos/basefoundry/base-cli/pulls\\?state=open\\&per_page=100) + printf '474\\t%s\\n' "$TEST_HEAD_SHA" + ;; + repos/basefoundry/base-cli/pulls/474) + printf '%s\\n' '{"number":474,"state":"open","user":{"login":"dependabot[bot]"},"head":{"sha":"'"$TEST_HEAD_SHA"'","ref":"dependabot/github_actions/astral-sh/setup-uv-10.3.1","repo":{"full_name":"basefoundry/base-cli"}}}' + ;; + repos/basefoundry/base-cli/commits/*) + printf '%s\\n' '{"author":{"login":"collaborator"},"commit":{"verification":{"verified":false,"reason":"unsigned"}}}' + ;; + repos/basefoundry/base-cli/statuses/*) + ;; + *) + printf 'unexpected gh api path: %s\\n' "$path" >&2 + exit 2 + ;; +esac +""" + + with tempfile.TemporaryDirectory() as directory: + fake_bin = Path(directory) / "bin" + fake_bin.mkdir() + gh_path = fake_bin / "gh" + gh_path.write_text(fake_gh, encoding="utf-8") + gh_path.chmod(gh_path.stat().st_mode | stat.S_IXUSR) + + environment = os.environ.copy() + environment.update( + { + "PATH": f"{fake_bin}:{environment['PATH']}", + "GITHUB_REPOSITORY": "basefoundry/base-cli", + "GITHUB_SERVER_URL": "https://github.com", + "GITHUB_RUN_ID": "123", + "GH_TOKEN": "test-token", + "POLICY_CONTEXT": "base/issue-branch-policy", + "PULL_REQUEST_NUMBER": "474", + "ISSUE_EVENT_NUMBER": "", + "ISSUE_EVENT_LABEL": "", + "DEFAULT_BRANCH": "main", + "EVENT_NAME": "pull_request_target", + "EVENT_HEAD_SHA": head_sha, + "DISPATCH_HEAD_SHA": "", + "PREVIOUS_HEAD_SHA": "", + "RUN_SHA": "b" * 40, + "TEST_HEAD_SHA": head_sha, + } + ) + completed = subprocess.run( + ["bash", "-euo", "pipefail", "-c", policy_run], + cwd=root, + env=environment, + capture_output=True, + text=True, + ) + + assert completed.returncode != 0 + assert "One or more PRs sharing this commit violate branch policy" in completed.stderr + assert "Validated Dependabot branch" not in completed.stdout From 765d67d368aba639633514f95361f858fbc1b1f7 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:36:33 +0530 Subject: [PATCH 4/4] test: make Dependabot policy regression portable --- tests/test_issue_branch_policy.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_issue_branch_policy.py b/tests/test_issue_branch_policy.py index 7c02112..0f2ab9c 100644 --- a/tests/test_issue_branch_policy.py +++ b/tests/test_issue_branch_policy.py @@ -1,15 +1,24 @@ from __future__ import annotations import os +import shutil import stat import subprocess import tempfile from pathlib import Path +import pytest import yaml def test_dependabot_policy_rejects_an_unverified_collaborator_head_commit() -> None: + if os.name == "nt": + pytest.skip("the policy under test is a Bash workflow and is validated on POSIX runners") + if shutil.which("bash") is None: + pytest.skip("bash is required to execute the workflow policy") + if shutil.which("jq") is None: + pytest.skip("jq is required by the workflow policy") + root = Path(__file__).resolve().parents[1] workflow = yaml.safe_load((root / ".github/workflows/issue-branch-policy.yml").read_text(encoding="utf-8")) policy_run = workflow["jobs"]["policy"]["steps"][0]["run"] @@ -52,7 +61,7 @@ def test_dependabot_policy_rejects_an_unverified_collaborator_head_commit() -> N environment = os.environ.copy() environment.update( { - "PATH": f"{fake_bin}:{environment['PATH']}", + "PATH": os.pathsep.join((str(fake_bin), environment["PATH"])), "GITHUB_REPOSITORY": "basefoundry/base-cli", "GITHUB_SERVER_URL": "https://github.com", "GITHUB_RUN_ID": "123",