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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "github-project",
"version": "2.10.2",
"version": "2.11.0",
"description": "GitHub repository setup, branch protection, and configuration",
"repository": "https://github.com/netresearch/github-project-skill",
"license": "(MIT AND CC-BY-SA-4.0)",
Expand Down
2 changes: 1 addition & 1 deletion skills/github-project/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ license: "(MIT AND CC-BY-SA-4.0). See LICENSE-MIT and LICENSE-CC-BY-SA-4.0"
compatibility: "Requires gh CLI, git."
metadata:
author: Netresearch DTT GmbH
version: "2.10.2"
version: "2.11.0"
repository: https://github.com/netresearch/github-project-skill
allowed-tools: Bash(gh:*) Bash(git:*) Bash(grep:*) Read Write
---
Expand Down
70 changes: 70 additions & 0 deletions skills/github-project/checkpoints.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,54 @@ mechanical:
severity: info
desc: "Auto-merge should dynamically detect merge strategy from repo settings"

# === BRANCH PROTECTION AUDIT ===
- id: GH-30
type: command
target: |
# Require GitHub CLI and a valid GitHub remote slug
command -v gh >/dev/null 2>&1 || exit 1
REPO_SLUG=$(git config --get remote.origin.url 2>/dev/null | sed -E 's|.*github\.com[:/](.+/[^.]+)(\.git)?$|\1|')
test -n "$REPO_SLUG" || exit 1
DEFAULT_BRANCH=$(gh api "repos/$REPO_SLUG" --jq '.default_branch' 2>/dev/null || echo 'main')
RESULT=$(gh api "repos/$REPO_SLUG/branches/$DEFAULT_BRANCH/protection" --jq '.enforce_admins.enabled' 2>/dev/null || echo '')
if [ "$RESULT" = "true" ]; then
exit 0
fi
# Fallback: check rulesets when classic branch protection is not configured
RULESET_OK=$(gh api "repos/$REPO_SLUG/rulesets" \
--jq 'map(select(.enforcement == "active" and .target == "branch" and ((.bypass_actors // []) | length == 0))) | any' \
2>/dev/null || echo 'false')
test "$RULESET_OK" = "true"
severity: error
desc: "enforce_admins must be true on default branch — without it admins can bypass required status checks and review requirements"

- id: GH-31
type: command
target: |
# Require GitHub CLI and a valid GitHub remote slug
command -v gh >/dev/null 2>&1 || exit 1
REPO_SLUG=$(git config --get remote.origin.url 2>/dev/null | sed -E 's|.*github\.com[:/](.+/[^.]+)(\.git)?$|\1|')
test -n "$REPO_SLUG" || exit 1
DEFAULT_BRANCH=$(gh api "repos/$REPO_SLUG" --jq '.default_branch' 2>/dev/null || echo 'main')
RESULT=$(gh api "repos/$REPO_SLUG/branches/$DEFAULT_BRANCH/protection" --jq '.required_conversation_resolution.enabled' 2>/dev/null || echo '')
if [ "$RESULT" = "true" ]; then
exit 0
fi
# Fallback: check rulesets for review thread resolution
JQ_FILTER='map(select(
.enforcement == "active" and .target == "branch"
and ((.bypass_actors // []) | length == 0)
and any(.rules[]?;
.type == "pull_request" and
(.parameters.required_review_thread_resolution // false)
)
)) | any'
RULESET_OK=$(gh api "repos/$REPO_SLUG/rulesets" \
--jq "$JQ_FILTER" 2>/dev/null || echo 'false')
test "$RULESET_OK" = "true"
severity: error
desc: "required_conversation_resolution must be enabled — combined with enforce_admins, ensures unresolved review threads block ALL merges including admins"

llm_reviews:
# === BRANCH PROTECTION + MERGE QUEUE COMPATIBILITY ===
- id: GH-22
Expand Down Expand Up @@ -221,6 +269,28 @@ llm_reviews:
severity: error
desc: "Verify auto-approve workflow correctly handles bot accounts (author_association is NONE for bots)"

# === BRANCH PROTECTION ENFORCEMENT AUDIT ===
- id: GH-32
domain: security
prompt: |
Audit branch protection enforcement on the default branch:
1. Run: gh api repos/OWNER/REPO/branches/BRANCH/protection --jq '.enforce_admins.enabled'
Must be true. Without enforce_admins, repository admins can bypass ALL
branch protection rules: required status checks, required reviews,
required conversation resolution, and signed commit requirements.
2. Run: gh api repos/OWNER/REPO/branches/BRANCH/protection --jq '.required_conversation_resolution.enabled'
Must be true. Without this, unresolved review threads do not block merges.
3. Verify BOTH are enabled together. enforce_admins alone is insufficient if
conversation resolution is not required (admins could merge with unresolved
feedback). Conversation resolution alone is insufficient if enforce_admins
is false (admins bypass it entirely).
4. If using rulesets instead of branch protection, check the equivalent:
gh api repos/OWNER/REPO/rulesets --jq '.[].rules[] | select(.type=="pull_request") | .parameters'
Look for required_review_thread_resolution: true and bypass_actors being empty.
Report which settings are missing and the specific security risk each creates.
severity: error
desc: "Verify enforce_admins and required_conversation_resolution are both enabled to prevent bypassing review requirements"

# === SUBJECTIVE CHECKS (require LLM judgment) ===
- id: GH-15
domain: repo-health
Expand Down
28 changes: 25 additions & 3 deletions skills/github-project/references/security-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,23 @@ When a GitHub org has an **Actions allow-list**, composite actions' **internal s
printf "%s\n" "/home/linuxbrew/.linuxbrew/bin" "/home/linuxbrew/.linuxbrew/sbin" >> "$GITHUB_PATH"
```

## Branch Protection: Enforce for Admins

`enforce_admins` **MUST be `true`** on the default branch. Without it, repository admins can bypass all branch protection rules including required status checks, required reviews, required conversation resolution, and signed commit requirements.

```bash
# Check current state
gh api repos/OWNER/REPO/branches/main/protection --jq '.enforce_admins.enabled'

# Enable enforce_admins
gh api repos/OWNER/REPO/branches/main/protection/enforce_admins -X POST

# Verify
gh api repos/OWNER/REPO/branches/main/protection --jq 'if .enforce_admins.enabled then "OK: Admin enforcement enabled" else "FAIL: Admins can bypass branch protection" end'
```

> **Security note:** Even with `required_conversation_resolution: true`, admins can merge with unresolved review threads if `enforce_admins` is `false`. Both settings must be enabled together for effective protection.

## Branch Protection: Required Reviews

All projects MUST have `required_approving_review_count >= 1`.
Expand Down Expand Up @@ -222,7 +239,7 @@ If `awaiting` is non-empty, the PR is **not ready to merge** -- those reviewers

## Required Conversation Resolution

All review threads on a PR **must be resolved** before merging:
All review threads on a PR **must be resolved** before merging. Combined with `enforce_admins: true`, this ensures unresolved review threads block **ALL** merges, including those by admins.

```bash
# Enable
Expand All @@ -234,8 +251,13 @@ gh api repos/OWNER/REPO/branches/main/protection -X PUT \
}
EOF

# Verify
gh api repos/OWNER/REPO/branches/main/protection --jq 'if .required_conversation_resolution.enabled then "OK: Conversation resolution required" else "FAIL: Conversation resolution NOT required - ENABLE IT" end'
# Verify both conversation resolution AND admin enforcement
gh api repos/OWNER/REPO/branches/main/protection --jq '{
conversation_resolution: .required_conversation_resolution.enabled,
enforce_admins: .enforce_admins.enabled
} | if .conversation_resolution and .enforce_admins then "OK: Review threads enforced for all users"
elif .conversation_resolution then "PARTIAL: Conversation resolution enabled but admins can bypass (enable enforce_admins)"
else "FAIL: Conversation resolution NOT required - ENABLE IT" end'

# List unresolved threads
gh api graphql -f query='query($owner:String!,$repo:String!,$pr:Int!){
Expand Down
58 changes: 58 additions & 0 deletions skills/github-project/scripts/verify-github-project.sh
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ echo " - Require approvals (1+ based on team size)"
echo " - Dismiss stale reviews on new commits"
echo " - Require review from CODEOWNERS"
echo " - Require conversation resolution"
echo " - Enforce for admins (enforce_admins)"
echo " - Do not allow force pushes"
echo " - Do not allow deletions"
echo " - Enable merge queue (with MERGE method)"
Expand All @@ -458,6 +459,63 @@ if [ -d ".github/workflows" ] && ls .github/workflows/*.yml > /dev/null 2>&1; th
pass "CI workflows present (enables required status checks)"
fi

# Check branch protection via API if gh CLI is available
if command -v gh &> /dev/null && [ -n "$REPO_SLUG" ]; then
BRANCH="${GH_DEFAULT:-main}"
PROTECTION=$(gh api "repos/$REPO_SLUG/branches/$BRANCH/protection" 2>/dev/null || echo "")

Comment thread
CybotTM marked this conversation as resolved.
if [ -n "$PROTECTION" ]; then
# Check enforce_admins
ENFORCE_ADMINS=$(echo "$PROTECTION" | jq -r '.enforce_admins.enabled // false')
if [ "$ENFORCE_ADMINS" = "true" ]; then
pass "enforce_admins enabled (admins cannot bypass branch protection)"
else
fail "enforce_admins disabled — admins can bypass required status checks and review requirements"
fi

# Check required_conversation_resolution
CONV_RESOLUTION=$(echo "$PROTECTION" | jq -r '.required_conversation_resolution.enabled // false')
if [ "$CONV_RESOLUTION" = "true" ]; then
pass "required_conversation_resolution enabled"
else
fail "required_conversation_resolution disabled — unresolved review threads do not block merges"
fi

# Combined check
if [ "$ENFORCE_ADMINS" = "true" ] && [ "$CONV_RESOLUTION" = "true" ]; then
pass "Review enforcement complete: unresolved threads block ALL merges including admins"
elif [ "$CONV_RESOLUTION" = "true" ] && [ "$ENFORCE_ADMINS" != "true" ]; then
warn "Conversation resolution enabled but admins can bypass it (enable enforce_admins)"
fi
else
# Fallback: check rulesets when classic branch protection is not configured
RULESETS=$(gh api "repos/$REPO_SLUG/rulesets" 2>/dev/null || echo "")
if [ -n "$RULESETS" ] && [ "$RULESETS" != "[]" ]; then
info "No classic branch protection found — checking rulesets"

# Check for active branch rulesets with no bypass actors (equivalent to enforce_admins)
RULESET_NO_BYPASS=$(echo "$RULESETS" | jq -r \
'map(select(.enforcement == "active" and .target == "branch" and ((.bypass_actors // []) | length == 0))) | any')
if [ "$RULESET_NO_BYPASS" = "true" ]; then
pass "Active branch ruleset with no bypass actors (equivalent to enforce_admins)"
else
fail "No active branch ruleset without bypass actors — admins may bypass protection"
fi

# Check for required_review_thread_resolution in rulesets
RULESET_CONV=$(echo "$RULESETS" | jq -r \
'map(select(.enforcement == "active" and .target == "branch" and any(.rules[]?; .type == "pull_request" and (.parameters.required_review_thread_resolution // false)))) | any')
if [ "$RULESET_CONV" = "true" ]; then
pass "Ruleset requires review thread resolution (equivalent to required_conversation_resolution)"
else
fail "No ruleset requiring review thread resolution — unresolved threads do not block merges"
fi
else
info "Could not fetch branch protection or rulesets (may not be configured or insufficient permissions)"
fi
fi
fi

# ─────────────────────────────────────────────────────────────────
header "Workflow Permissions"
# ─────────────────────────────────────────────────────────────────
Expand Down
Loading