Skip to content

Commit 4d7a49e

Browse files
feat(rules): wire 4 new rule modules through the facade (#326)
## 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)
1 parent ead5826 commit 4d7a49e

1 file changed

Lines changed: 82 additions & 2 deletions

File tree

lib/rules/rules.ex

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ defmodule Hypatia.Rules do
2525
alias Hypatia.Rules.MigrationRules
2626
alias Hypatia.Rules.BaselineHealth
2727
alias Hypatia.Rules.BuildSystemRules
28+
alias Hypatia.Rules.WorkflowHardening
29+
alias Hypatia.Rules.SupplyChain
30+
alias Hypatia.Rules.BranchProtection
31+
# alias Hypatia.Rules.ResearchExtensions # wired in follow-up after PR #325 merges
2832

2933
@doc """
3034
Run a comprehensive scan on a file's content given its path and language.
@@ -400,12 +404,88 @@ defmodule Hypatia.Rules do
400404
defdelegate detect_waste(repo_info), to: CicdRules
401405

402406
@doc """
403-
Run baseline-health checks (BH001-BH003): missing required_status_checks
407+
Run baseline-health checks (BH001-BH007): missing required_status_checks
404408
on main, deferred-migration TODOs in dep manifests, persistent >24h red
405-
baseline on main. Returns the same shape as `GitState.scan/1`.
409+
baseline on main, dead action SHA pins, push-only required checks,
410+
required-check drift, signing-key UID gaps. Returns `%{findings, total,
411+
by_severity, dispatch}`.
406412
"""
407413
defdelegate scan_baseline_health(repo_path, opts \\ []), to: BaselineHealth, as: :scan
408414

415+
@doc """
416+
Run workflow-content hardening checks (WH001-WH012): template injection,
417+
excessive permissions, dangerous triggers, unpinned actions, hardcoded
418+
credentials, missing timeouts/concurrency, secrets leakage, deprecated
419+
workflow commands, curl-pipe-shell, $GITHUB_ENV taint. Pure local file
420+
scan — no GitHub API.
421+
"""
422+
defdelegate scan_workflow_hardening(repo_path, opts \\ []), to: WorkflowHardening, as: :scan
423+
424+
@doc """
425+
Run supply-chain integrity checks (SC001-SC011): CODEOWNERS coverage,
426+
Dependabot config, archived actions, typosquats, pull_request_target,
427+
release SBOM/signing, self-hosted runners, OIDC vs static secrets,
428+
SECURITY.md, webhook secrets. Uses GitHub API when `:owner_repo` is
429+
supplied; degrades cleanly without a token.
430+
"""
431+
defdelegate scan_supply_chain(repo_path, opts \\ []), to: SupplyChain, as: :scan
432+
433+
@doc """
434+
Run branch-protection hygiene checks (BP001-BP007) against the default
435+
branch: required signatures, linear history, required reviews, stale-
436+
review dismissal, CODEOWNERS, admin enforcement, force-push/delete
437+
block. All API-backed; returns `[]` cleanly without a token.
438+
"""
439+
defdelegate scan_branch_protection(owner, repo), to: BranchProtection, as: :scan
440+
441+
# ResearchExtensions (RE001-RE010) delegate added in follow-up once
442+
# PR #325 lands on main. The facade for the other four families is
443+
# below.
444+
445+
@doc """
446+
Run every estate-policy rule available against a repository in one
447+
pass and merge the findings. Optional `:owner_repo` keyword unlocks
448+
the API-backed rules in `BaselineHealth`, `SupplyChain`, and
449+
`BranchProtection`.
450+
451+
Returns `%{findings: [...], total: N, by_severity: %{...}, dispatch: [...]}`
452+
with all five new rule families plus the existing `BaselineHealth`
453+
surface combined.
454+
"""
455+
def scan_all_estate_policies(repo_path, opts \\ []) do
456+
{owner, repo} =
457+
case Keyword.get(opts, :owner_repo) do
458+
{o, r} when is_binary(o) and is_binary(r) -> {o, r}
459+
_ -> {nil, nil}
460+
end
461+
462+
parts = [
463+
BaselineHealth.scan(repo_path, opts),
464+
WorkflowHardening.scan(repo_path, opts),
465+
SupplyChain.scan(repo_path, opts)
466+
]
467+
468+
parts =
469+
if owner && repo do
470+
[BranchProtection.scan(owner, repo) | parts]
471+
else
472+
parts
473+
end
474+
475+
findings = Enum.flat_map(parts, & &1.findings)
476+
477+
%{
478+
findings: findings,
479+
total: length(findings),
480+
by_severity:
481+
findings
482+
|> Enum.group_by(& &1.severity)
483+
|> Enum.map(fn {sev, items} -> {sev, length(items)} end)
484+
|> Map.new(),
485+
dispatch: Enum.flat_map(parts, & &1.dispatch)
486+
}
487+
end
488+
409489
# ---------------------------------------------------------------------------
410490
# Private Helpers
411491
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)