feat(rules): SupplyChain (SC001-SC011) — 11 rules from Scorecard/SLSA/OWASP/Endor#322
Merged
Conversation
…SA + OWASP New module Hypatia.Rules.SupplyChain covers provenance and integrity defects — complementary to WorkflowHardening (content) and BaselineHealth (drift). ## Rules | ID | Detects | Severity | Upstream | |---|---|---|---| | SC001 | .github/workflows/ not in CODEOWNERS | warn | GH hardening | | SC002 | Dependabot missing or no github-actions ecosystem | warn/info | Scorecard, GH hardening | | SC003 | Action repo archived (API-checked) | warn | zizmor archived-uses | | SC004 | Typosquat action name (edit-distance to canonical) | critical | zizmor typosquat, Gu et al. 2023 | | SC005 | pull_request_target trigger present (broad signal) | high | scorecard Dangerous-Workflow | | SC006 | Release workflow without SBOM emission | warn | NIST SSDF PS.2.1, scorecard SBOM | | SC007 | Self-hosted runner in public repo (API-checked) | critical | GH hardening | | SC008 | Publish via long-lived secret instead of OIDC | warn | GH hardening, zizmor use-trusted-publishing | | SC009 | SECURITY.md missing | warn | scorecard Security-Policy | | SC010 | Repo webhook without secret (API-checked) | critical | scorecard Webhooks | | SC011 | Release workflow without signing/provenance | high | scorecard Signed-Releases, Endor R-END-02 | ## Tests 18 tests / 0 failures. Coverage spans positive cases per rule, negative cases (clean fixtures pass), and a no-token regression test for the API-backed rules (SC003, SC007, SC010). ## Dispatch routing SC001, SC002, SC009 are mechanically fixable (add file/section) → rhodibot at 0.88-0.92. Others route to sustainabot at confidence by severity.
🔍 Hypatia Security ScanFindings: 116 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: 116 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
…corecard / NIST SSDF (#323) ## Summary New module `Hypatia.Rules.BranchProtection` adds 7 rules covering review-and-integrity controls on the default branch. Complements `BaselineHealth` (drift, #320), `WorkflowHardening` (content, #321), and `SupplyChain` (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: ``` gh api repos/{owner}/{repo}/branches/main/protection ``` ## Rules | ID | Detects | Severity | Provenance | |---|---|---|---| | BP001 | `required_signatures != true` (unsigned commits can land) | high | CIS GH 1.1.x | | BP002 | `required_linear_history != true` (merge commits obscure audit trail) | warn | CIS GH 1.3.x | | BP003 | `required_approving_review_count < 1` (PRs self-mergeable) | critical | CIS GH 1.4.x | | BP004 | `dismiss_stale_reviews != true` (approvals carry across new commits) | warn | CIS GH 1.5.x | | BP005 | `require_code_owner_reviews: true` but CODEOWNERS missing/empty | high | CIS GH 1.6.x + NIST SP 800-218 PO.3.2 | | BP006 | `enforce_admins != true` (admins bypass every other protection) | high | CIS GH | | BP007 | `allow_force_pushes` or `allow_deletions` (history-integrity break) | critical | scorecard `Branch-Protection` tier 1 | ## Tests `mix test test/branch_protection_test.exs` → **15 tests / 0 failures**: - One **no-token regression** per rule (7 tests) — each `bpNNN_*` returns `[]` cleanly when neither `GITHUB_TOKEN` nor `HYPATIA_DISPATCH_PAT` is set. Mirrors the convention in `BaselineHealth` and `SupplyChain`. - **`scan/2` facade 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). - **Module surface** (2 tests) — all seven `bpNNN_*` functions plus `scan/2` and `dispatch_recommendations/1` are 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/1` and `fetch_branch_protection/3` are direct ports of the `BaselineHealth` helpers (same auth header set, same JSON decode path, same `{:error, :no_token}` short-circuit). Owner/repo extraction from the `origin` remote also mirrors `BaselineHealth` for the no-`owner_repo`-passed case. `System.cmd("curl", [...])` form is used throughout per the hypatia CLAUDE.md scanner-hygiene rules (no `System.shell`, no `:os.cmd`). `fetch_branch_protection/3` distinguishes `{: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](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.SupplyChain` covers provenance and integrity defects across 11 rules, complementary to `WorkflowHardening` (content defects, PR #321) and `BaselineHealth` (drift, PR #320).
Tests
`mix test test/supply_chain_test.exs` → 18 tests / 0 failures. Coverage includes positive cases per rule, negative-case clean fixtures, and a no-token regression group for the API-backed rules (SC003, SC007, SC010).
Dispatch
Mechanically-fixable rules (SC001, SC002, SC009 — add file/section) route to rhodibot at 0.88-0.92 confidence. The rest are advisory → sustainabot at confidence calibrated by severity.
🤖 Generated with Claude Code