feat(rules): BranchProtection (BP001-BP007) — 7 rules from CIS GH / Scorecard / NIST SSDF#323
Merged
Merged
Conversation
…corecard / NIST SSDF
New module `Hypatia.Rules.BranchProtection` covers review-and-integrity
controls on the default branch. Complements `BaselineHealth` (drift),
`WorkflowHardening` (content), and `SupplyChain` (provenance).
All seven rules read from a single GitHub API endpoint:
`gh api repos/{owner}/{repo}/branches/main/protection`.
## Rules
| ID | Detects | Severity | Source |
|---|---|---|---|
| BP001 | required_signatures != true | high | CIS GH 1.1.x |
| BP002 | required_linear_history != true | warn | CIS GH 1.3.x |
| BP003 | required_approving_review_count < 1 | critical | CIS GH 1.4.x |
| BP004 | dismiss_stale_reviews != true | warn | CIS GH 1.5.x |
| BP005 | require_code_owner_reviews but CODEOWNERS missing | high | CIS GH 1.6.x + NIST PO.3.2 |
| BP006 | enforce_admins != true | high | CIS GH |
| BP007 | allow_force_pushes or allow_deletions | critical | scorecard Branch-Protection tier 1 |
## Tests
15 tests covering:
- One no-token regression per rule (7 tests) — each `bpNNN_*` returns `[]`
cleanly when neither `GITHUB_TOKEN` nor `HYPATIA_DISPATCH_PAT` is set.
- `scan/2` facade shape (3 tests) — standard `{findings, total,
by_severity, dispatch}` map; empty result when no token / no origin.
- `dispatch_recommendations/1` (3 tests) — every BP rule routes to
`:sustainabot`; severity-to-confidence calibration matches the
documented table (critical 0.92, high 0.85, warn 0.75, info 0.60).
- Module surface (2 tests) — verifies all seven `bpNNN_*` functions plus
`scan/2` and `dispatch_recommendations/1` are exported.
## Dispatch routing
Every BP rule is owner-side (branch-protection edits require admin
scope on the repository, which the bot fleet does not have). All seven
findings route to `:sustainabot` with severity-derived confidence.
## API style
`curl_github/1` and `fetch_branch_protection/3` are direct ports of the
`BaselineHealth` helpers (same auth header set, same JSON decode, same
`{:error, :no_token}` short-circuit). Owner/repo extraction from
`origin` remote also mirrors `BaselineHealth` for the no-`owner_repo`
case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🔍 Hypatia Security ScanFindings: 110 issues detected
View findings[
{
"reason": "Docker reference in Nickel config -- Podman/Containerfile highly preferred (Docker permitted) (1 occurrences, CWE-1104)",
"type": "ncl_docker_not_podman",
"file": "/home/runner/work/hypatia/hypatia/.machine_readable/svc/k9/hypatia-metadata.k9.ncl",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (2 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/cli/src/commands/batch.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap_or(0) with dangerous default (1 occurrences, CWE-754)",
"type": "unwrap_dangerous_default",
"file": "/home/runner/work/hypatia/hypatia/cli/src/commands/batch.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (1 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/cli/src/commands/scan.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap_or(0) with dangerous default (1 occurrences, CWE-754)",
"type": "unwrap_dangerous_default",
"file": "/home/runner/work/hypatia/hypatia/cli/src/commands/scan.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (2 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/cli/src/commands/fleet.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (2 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/cli/src/output.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (2 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/cli/build.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (3 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/fixer/src/main.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "expect() in hot path (5 occurrences, CWE-754)",
"type": "expect_in_hot_path",
"file": "/home/runner/work/hypatia/hypatia/fixer/src/scanner.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
}
]Powered by Hypatia Neurosymbolic CI/CD Intelligence |
🔍 Hypatia Security ScanFindings: 110 issues detected
View findings[
{
"reason": "Docker reference in Nickel config -- Podman/Containerfile highly preferred (Docker permitted) (1 occurrences, CWE-1104)",
"type": "ncl_docker_not_podman",
"file": "/home/runner/work/hypatia/hypatia/.machine_readable/svc/k9/hypatia-metadata.k9.ncl",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (2 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/cli/src/commands/batch.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap_or(0) with dangerous default (1 occurrences, CWE-754)",
"type": "unwrap_dangerous_default",
"file": "/home/runner/work/hypatia/hypatia/cli/src/commands/batch.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (1 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/cli/src/commands/scan.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap_or(0) with dangerous default (1 occurrences, CWE-754)",
"type": "unwrap_dangerous_default",
"file": "/home/runner/work/hypatia/hypatia/cli/src/commands/scan.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (2 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/cli/src/commands/fleet.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (2 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/cli/src/output.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (2 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/cli/build.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "unwrap() without prior check -- DoS via panic (3 occurrences, CWE-754)",
"type": "unwrap_without_check",
"file": "/home/runner/work/hypatia/hypatia/fixer/src/main.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
},
{
"reason": "expect() in hot path (5 occurrences, CWE-754)",
"type": "expect_in_hot_path",
"file": "/home/runner/work/hypatia/hypatia/fixer/src/scanner.rs",
"action": "flag",
"rule_module": "code_safety",
"severity": "low"
}
]Powered by Hypatia Neurosymbolic CI/CD Intelligence |
hyperpolymath
added a commit
that referenced
this pull request
May 26, 2026
## Summary Adds delegates on \`Hypatia.Rules\` for the rule families landed in PRs #320, #321, #322, #323. The new modules were independently usable but invisible from the top-level facade; this PR closes that gap. ## New facade surface | Function | Delegates to | Rules | |---|---|---| | \`scan_baseline_health/2\` | \`BaselineHealth.scan\` (existing; docstring updated for BH007) | BH001-BH007 | | \`scan_workflow_hardening/2\` | \`WorkflowHardening.scan\` | WH001-WH012 | | \`scan_supply_chain/2\` | \`SupplyChain.scan\` | SC001-SC011 | | \`scan_branch_protection/2\` | \`BranchProtection.scan\` (API-backed; takes \`owner, repo\`) | BP001-BP007 | | \`scan_all_estate_policies/2\` | — runs all four families and merges results | 34 rules total | ## Why combined-scan helper Callers (\`pattern_analyzer\`, \`triangle_router\`, \`fleet_dispatcher\`) already treat \`Hypatia.Rules\` as the entrypoint. Without these delegates the new modules are reachable only via fully-qualified names, which works but inverts the facade pattern adopted in #316. ## RE001-RE010 (PR #325) not included PR #325 (ResearchExtensions) is still OPEN at the time of this PR. Adding its alias here would break compilation on \`main\` for the window between this PR landing and #325 landing. A follow-up commit will wire \`ResearchExtensions\` once #325 merges — tracked inline at the alias block as a \`# wired in follow-up\` comment. ## Test plan - Existing 18-test \`BaselineHealth\` suite still passes (untouched). - Module compile-checks pass for the four families + facade. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New module
Hypatia.Rules.BranchProtectionadds 7 rules covering review-and-integrity controls on the default branch. ComplementsBaselineHealth(drift, #320),WorkflowHardening(content, #321), andSupplyChain(provenance, #322) — together the four modules cover the four facets of estate-wide CI/CD hygiene.All seven rules read from a single GitHub API endpoint:
Rules
required_signatures != true(unsigned commits can land)required_linear_history != true(merge commits obscure audit trail)required_approving_review_count < 1(PRs self-mergeable)dismiss_stale_reviews != true(approvals carry across new commits)require_code_owner_reviews: truebut CODEOWNERS missing/emptyenforce_admins != true(admins bypass every other protection)allow_force_pushesorallow_deletions(history-integrity break)Branch-Protectiontier 1Tests
mix test test/branch_protection_test.exs→ 15 tests / 0 failures:bpNNN_*returns[]cleanly when neitherGITHUB_TOKENnorHYPATIA_DISPATCH_PATis set. Mirrors the convention inBaselineHealthandSupplyChain.scan/2facade shape (3 tests) — standard{findings, total, by_severity, dispatch}map; empty findings when no token / no origin remote.dispatch_recommendations/1(3 tests) — every BP rule routes to:sustainabot; severity→confidence calibration matches the documented table (critical 0.92, high 0.85, warn 0.75, info 0.60).bpNNN_*functions plusscan/2anddispatch_recommendations/1are exported at the expected arities.Dispatch routing
Every BP rule routes to
:sustainabot. Branch-protection edits require admin scope on the repository, which the bot fleet does not hold — the finding is necessarily advisory only. Confidence is derived from severity (0.92 / 0.85 / 0.75 / 0.60 by tier).This is a deliberate divergence from
SupplyChain's mixed routing (where SC001/SC002/SC009 mechanically route to rhodibot at higher confidence because they're file-add fixes). No BP rule has a mechanical fix that a bot can ship — every one is an owner decision against the live repo settings.API style
curl_github/1andfetch_branch_protection/3are direct ports of theBaselineHealthhelpers (same auth header set, same JSON decode path, same{:error, :no_token}short-circuit). Owner/repo extraction from theoriginremote also mirrorsBaselineHealthfor the no-owner_repo-passed case.System.cmd("curl", [...])form is used throughout per the hypatia CLAUDE.md scanner-hygiene rules (noSystem.shell, no:os.cmd).fetch_branch_protection/3distinguishes{:error, :not_protected}(the API's"Branch not protected"message) from{:error, :no_token}and{:error, "GitHub API: ..."}— every BP rule treats all error cases as[](no false positives without ground truth).Module summary
lib/rules/branch_protection.ex— 644 lines.Single endpoint, seven rules, pattern-match-on-JSON-shape predicate per protection flag.
🤖 Generated with Claude Code