From e95862b5109ba50d306bf4086146d010d1a56351 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 22:35:34 +0000 Subject: [PATCH] fix(rules): know the standards repository under its new name hyperpolymath/standards moved to metadatastician/standards on 2026-10-25. Rules keyed on the old name went quiet for anything under the new one (Codex on #958): - ShaBumpPropagation: merge events from the moved repo carry the new name, and matched_reusables/1 compares names exactly, so no propagation finding fired. Both names are now registered. - MergeOrchestration.Sensor: standards contractiles/ and rulesets/ were meta (never self-approved) only for "hyperpolymath/standards"; events under the new name classified them as object. Either name, any case. - SecurityErrors pin exemption for own-org standards reusables, and WorkflowAudit WF016 (orphan pin) / WF027 (measured-dead revision): matched only the old path. Either name now. - ci-health action-superset: the metadatastician/standards reusable paths, which the hyperpolymath/* owner pattern does not cover. Hypatia.Rules.StandardsRepo holds the names (current first) so the next rule asks one place. Also fixes remediate.sh's allow-list body: jq parsed `["hyperpolymath/*"] + . as $pats | {...}` as array + object, so the B-ALLOWLIST remediation could never build its request and test/ci-health-test.sh exited 5 on main. Parenthesised. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM --- lib/merge_orchestration/sensor.ex | 2 +- lib/rules/security_errors.ex | 15 ++++--- lib/rules/sha_bump_propagation.ex | 47 +++++++++++++-------- lib/rules/standards_repo.ex | 53 ++++++++++++++++++++++++ lib/rules/workflow_audit.ex | 45 ++++++++++---------- scripts/ci-health/action-superset.txt | 13 ++++++ scripts/ci-health/remediate.sh | 2 +- test/merge_orchestration/sensor_test.exs | 6 +++ test/rules/sha_bump_propagation_test.exs | 22 +++++++++- test/rules/standards_repo_test.exs | 43 +++++++++++++++++++ test/rules/workflow_audit_pins_test.exs | 23 ++++++++-- test/security_errors_test.exs | 8 ++++ 12 files changed, 227 insertions(+), 52 deletions(-) create mode 100644 lib/rules/standards_repo.ex create mode 100644 test/rules/standards_repo_test.exs diff --git a/lib/merge_orchestration/sensor.ex b/lib/merge_orchestration/sensor.ex index a178cc7a..758bd5ec 100644 --- a/lib/merge_orchestration/sensor.ex +++ b/lib/merge_orchestration/sensor.ex @@ -128,7 +128,7 @@ defmodule Hypatia.MergeOrchestration.Sensor do String.contains?(p, "lib/rules/") or String.contains?(p, "merge-orchestration/schemas") or String.contains?(p, "merge-orchestration/ci-templates") or - (repo == "hyperpolymath/standards" and + (Hypatia.Rules.StandardsRepo.name?(repo) and (String.contains?(p, "contractiles/") or String.contains?(p, "rulesets/"))) end diff --git a/lib/rules/security_errors.ex b/lib/rules/security_errors.ex index 6e1d7d62..b522f35a 100644 --- a/lib/rules/security_errors.ex +++ b/lib/rules/security_errors.ex @@ -336,6 +336,12 @@ defmodule Hypatia.Rules.SecurityErrors do # rationale to dismiss the Scorecard alert. Keyed by action-name prefix so # every version and the `/.github/workflows/...` path form is covered. # Refs hyperpolymath/hypatia#262. + @own_org_wrapper_reason "Own-org reusable workflow referenced at @main is the canonical " <> + "wrapper pattern (project_admin_merge_wrappers_2026_05_26). " <> + "SHA-pinning each wrapper would force a fan-out SHA-bump PR " <> + "per consumer repo per reusable change — defeating the wrapper's " <> + "whole purpose. Trust boundary: own-org commit rights." + @pin_exempt %{ "slsa-framework/slsa-github-generator" => "SLSA generator self-verifies its own github.ref to mint verifiable " <> @@ -352,12 +358,9 @@ defmodule Hypatia.Rules.SecurityErrors do # dependency you have commit rights on. Observed flagging 2026-05-27 # across affinescript#357 / panll#54 / claude-integrations#43 / # idaptik#98 / tma-mark2#41 / maa-framework#78 (6/6 PRs flagged). - "hyperpolymath/standards/.github/workflows/" => - "Own-org reusable workflow referenced at @main is the canonical " <> - "wrapper pattern (project_admin_merge_wrappers_2026_05_26). " <> - "SHA-pinning each wrapper would force a fan-out SHA-bump PR " <> - "per consumer repo per reusable change — defeating the wrapper's " <> - "whole purpose. Trust boundary: own-org commit rights." + "hyperpolymath/standards/.github/workflows/" => @own_org_wrapper_reason, + # The same repository under its current name (Hypatia.Rules.StandardsRepo). + "metadatastician/standards/.github/workflows/" => @own_org_wrapper_reason } def pin_exempt, do: @pin_exempt diff --git a/lib/rules/sha_bump_propagation.ex b/lib/rules/sha_bump_propagation.ex index c667e044..7aa487b1 100644 --- a/lib/rules/sha_bump_propagation.ex +++ b/lib/rules/sha_bump_propagation.ex @@ -60,26 +60,37 @@ defmodule Hypatia.Rules.ShaBumpPropagation do # Estate reusable-workflow registry. Sourced from hypatia#418 issue body. # Each entry: `{repo, workflow_path}` — matches against `event.source_repo` # and any file in `event.files_changed`. - @known_reusables [ - {"hyperpolymath/standards", ".github/workflows/governance-reusable.yml"}, - {"hyperpolymath/standards", ".github/workflows/rust-ci-reusable.yml"}, - {"hyperpolymath/standards", ".github/workflows/deno-ci-reusable.yml"}, - {"hyperpolymath/standards", ".github/workflows/elixir-ci-reusable.yml"}, - {"hyperpolymath/standards", ".github/workflows/scorecard-reusable.yml"}, - {"hyperpolymath/standards", ".github/workflows/secret-scanner-reusable.yml"}, - {"hyperpolymath/standards", ".github/workflows/codeql-reusable.yml"}, - {"hyperpolymath/standards", ".github/workflows/hypatia-scan-reusable.yml"}, - {"hyperpolymath/standards", ".github/workflows/mirror-reusable.yml"}, - {"hyperpolymath/standards", ".github/workflows/changelog-reusable.yml"}, - # hyperpolymath/cicd-suite (cicd-suite#54): the estate audit and the four - # language gates, consumed as thin callers at a full cicd-suite SHA. - {"hyperpolymath/cicd-suite", ".github/workflows/main-estate-audit.yml"}, - {"hyperpolymath/cicd-suite", ".github/workflows/rust-ci.yml"}, - {"hyperpolymath/cicd-suite", ".github/workflows/creusot-gate.yml"}, - {"hyperpolymath/cicd-suite", ".github/workflows/spark-gate.yml"}, - {"hyperpolymath/cicd-suite", ".github/workflows/kanren-gate.yml"} + @standards_reusables [ + ".github/workflows/governance-reusable.yml", + ".github/workflows/rust-ci-reusable.yml", + ".github/workflows/deno-ci-reusable.yml", + ".github/workflows/elixir-ci-reusable.yml", + ".github/workflows/scorecard-reusable.yml", + ".github/workflows/secret-scanner-reusable.yml", + ".github/workflows/codeql-reusable.yml", + ".github/workflows/hypatia-scan-reusable.yml", + ".github/workflows/mirror-reusable.yml", + ".github/workflows/changelog-reusable.yml" ] + # The standards reusables under every name of the repository: merge events + # from it now carry `metadatastician/standards`, and the estate still holds + # pins under the former name (Hypatia.Rules.StandardsRepo). + @known_reusables for( + repo <- Hypatia.Rules.StandardsRepo.names(), + path <- @standards_reusables, + do: {repo, path} + ) ++ + [ + # hyperpolymath/cicd-suite (cicd-suite#54): the estate audit and the four + # language gates, consumed as thin callers at a full cicd-suite SHA. + {"hyperpolymath/cicd-suite", ".github/workflows/main-estate-audit.yml"}, + {"hyperpolymath/cicd-suite", ".github/workflows/rust-ci.yml"}, + {"hyperpolymath/cicd-suite", ".github/workflows/creusot-gate.yml"}, + {"hyperpolymath/cicd-suite", ".github/workflows/spark-gate.yml"}, + {"hyperpolymath/cicd-suite", ".github/workflows/kanren-gate.yml"} + ] + # Composite actions (referenced via `uses: /@`) also flow # through this rule — the workflow_path is the conventional `action.yml` # at the action repo root. diff --git a/lib/rules/standards_repo.ex b/lib/rules/standards_repo.ex new file mode 100644 index 00000000..feaa2424 --- /dev/null +++ b/lib/rules/standards_repo.ex @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +defmodule Hypatia.Rules.StandardsRepo do + @moduledoc """ + The names of the estate standards repository. + + `hyperpolymath/standards` moved to `metadatastician/standards` on + 2026-10-25. GitHub redirects git and the REST API for the old name, but not + a reusable workflow's `uses:`: a caller still naming the old repository ends + in `startup_failure` ("workflow was not found"). Callers, lock records and + merge events under the old name stay in the estate until each repository is + re-pointed, so every rule that keys on the standards repository accepts + both names. The current name comes first. + """ + + @current "metadatastician/standards" + @former ["hyperpolymath/standards"] + + @doc "The repository's current name." + def current, do: @current + + @doc "Every name the repository is known by, current first." + def names, do: [@current | @former] + + @doc "True when `repo` (`owner/name`, any case) is the standards repository." + def name?(repo) when is_binary(repo) do + downcased = String.downcase(repo) + Enum.any?(names(), &(&1 == downcased)) + end + + def name?(_), do: false + + @doc """ + Regex source for `path` (`owner/repo/rest`) that matches it under any name + of the standards repository, the `owner/repo` part case-insensitively (as + GitHub resolves it). A path outside the standards repository is escaped + as it stands. + """ + def path_regex_source(path) when is_binary(path) do + case String.split(path, "/", parts: 3) do + [owner, repo, rest] -> + if name?(owner <> "/" <> repo) do + alternation = Enum.map_join(names(), "|", &Regex.escape/1) + "(?i:(?:#{alternation}))/#{Regex.escape(rest)}" + else + Regex.escape(path) + end + + _ -> + Regex.escape(path) + end + end +end diff --git a/lib/rules/workflow_audit.ex b/lib/rules/workflow_audit.ex index 0f96cb0b..413d45b1 100644 --- a/lib/rules/workflow_audit.ex +++ b/lib/rules/workflow_audit.ex @@ -205,10 +205,10 @@ defmodule Hypatia.Rules.WorkflowAudit do [_, name] = Regex.run(~r/^ ([A-Za-z0-9_-]+):/, line) {MapSet.put(seen, name), with_to, reusable, in_j, name} - (in_j and curj) && String.match?(line, ~r/^ timeout-minutes:\s*\d+/) -> + in_j and curj && String.match?(line, ~r/^ timeout-minutes:\s*\d+/) -> {seen, MapSet.put(with_to, curj), reusable, in_j, curj} - (in_j and curj) && String.match?(line, ~r/^ uses:\s*\S/) -> + in_j and curj && String.match?(line, ~r/^ uses:\s*\S/) -> {seen, with_to, MapSet.put(reusable, curj), in_j, curj} String.match?(line, ~r/^[A-Za-z]/) -> @@ -877,11 +877,9 @@ defmodule Hypatia.Rules.WorkflowAudit do Enum.flat_map(workflow_contents, fn {filename, content} -> stripped = strip_comments(content) - uses_scorecard? = - Regex.match?(~r/uses:\s*ossf\/scorecard-action@/, stripped) + uses_scorecard? = Regex.match?(~r/uses:\s*ossf\/scorecard-action@/, stripped) - publish_true? = - Regex.match?(~r/publish_results:\s*true/, stripped) + publish_true? = Regex.match?(~r/publish_results:\s*true/, stripped) # Only emit if the job that runs `ossf/scorecard-action` with `publish_results: true` # itself contains a `run:` step. We need to check per-job, not per-file. @@ -981,8 +979,7 @@ defmodule Hypatia.Rules.WorkflowAudit do uses_checkout? = Regex.match?(~r/uses:\s*actions\/checkout@/, stripped) - has_user_root? = - Regex.match?(~r/options:\s*[^\n]*--user\s+(?:root|0)\b/, stripped) + has_user_root? = Regex.match?(~r/options:\s*[^\n]*--user\s+(?:root|0)\b/, stripped) if uses_checkout? and not has_user_root? do Enum.flat_map(@nonroot_container_images, fn img -> @@ -1079,7 +1076,9 @@ defmodule Hypatia.Rules.WorkflowAudit do Enum.flat_map(@orphan_reusable_shas, fn {reusable_path, orphan_shas} -> # Match: `uses: owner/repo/.github/workflows/x-reusable.yml@SHA` - ref_re = ~r/uses:\s*#{Regex.escape(reusable_path)}@([0-9a-f]{8,40})/ + # Under either name of the standards repository (StandardsRepo). + ref_re = + ~r/uses:\s*#{Hypatia.Rules.StandardsRepo.path_regex_source(reusable_path)}@([0-9a-f]{8,40})/ Regex.scan(ref_re, stripped) |> Enum.flat_map(fn [_full, sha] -> @@ -2907,8 +2906,17 @@ defmodule Hypatia.Rules.WorkflowAudit do # case-insensitively and rewrites it as the caller spells it. defp wf027_reusable_re(reusable) do case String.split(reusable, "/", parts: 3) do - [owner, repo, path] -> "(?i:#{Regex.escape(owner <> "/" <> repo)})/#{Regex.escape(path)}" - _ -> Regex.escape(reusable) + [owner, repo, path] -> + if Hypatia.Rules.StandardsRepo.name?(owner <> "/" <> repo) do + # A standards reusable is the same call under either name of the + # repository (Hypatia.Rules.StandardsRepo). + Hypatia.Rules.StandardsRepo.path_regex_source(reusable) + else + "(?i:#{Regex.escape(owner <> "/" <> repo)})/#{Regex.escape(path)}" + end + + _ -> + Regex.escape(reusable) end end @@ -3291,11 +3299,9 @@ defmodule Hypatia.Rules.WorkflowAudit do [] true -> - uses_workflow_sha? = - Regex.match?(~r/\$\{\{\s*github\.workflow_sha\s*\}\}/, ref_value) + uses_workflow_sha? = Regex.match?(~r/\$\{\{\s*github\.workflow_sha\s*\}\}/, ref_value) - caller_repo? = - Regex.match?(~r/\$\{\{\s*github\.repository\s*\}\}/, repo_value) + caller_repo? = Regex.match?(~r/\$\{\{\s*github\.repository\s*\}\}/, repo_value) if uses_workflow_sha? and not caller_repo? do [ @@ -3350,8 +3356,7 @@ defmodule Hypatia.Rules.WorkflowAudit do variable. """ def check_reusable_caller_context_self_checkout(workflow_contents) do - caller_ref_re = - ~r/\$\{\{\s*github\.(?:ref|head_ref|sha|workflow_sha)\s*\}\}/ + caller_ref_re = ~r/\$\{\{\s*github\.(?:ref|head_ref|sha|workflow_sha)\s*\}\}/ Enum.flat_map(workflow_contents, fn {filename, content} -> stripped = strip_comments(content) @@ -3369,8 +3374,7 @@ defmodule Hypatia.Rules.WorkflowAudit do [] true -> - caller_repo_var? = - Regex.match?(~r/\$\{\{\s*github\.repository\s*\}\}/, repo_value) + caller_repo_var? = Regex.match?(~r/\$\{\{\s*github\.repository\s*\}\}/, repo_value) foreign_literal_self? = not caller_repo_var? and @@ -4203,8 +4207,7 @@ defmodule Hypatia.Rules.WorkflowAudit do Enum.flat_map(workflow_contents, fn {filename, content} -> stripped = strip_comments(content) - chapel? = - Enum.any?(@chapel_marker_patterns, &Regex.match?(&1, stripped)) + chapel? = Enum.any?(@chapel_marker_patterns, &Regex.match?(&1, stripped)) if chapel? do runs_on_vals = diff --git a/scripts/ci-health/action-superset.txt b/scripts/ci-health/action-superset.txt index d87ec0a1..8a11caae 100644 --- a/scripts/ci-health/action-superset.txt +++ b/scripts/ci-health/action-superset.txt @@ -108,6 +108,19 @@ hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml hyperpolymath/standards/.github/workflows/spark-theatre-gate.yml jlumbroso/free-disk-space leanprover/lean-action +metadatastician/standards/.github/workflows/changelog-reusable.yml +metadatastician/standards/.github/workflows/codeql-reusable.yml +metadatastician/standards/.github/workflows/deno-ci-reusable.yml +metadatastician/standards/.github/workflows/elixir-ci-reusable.yml +metadatastician/standards/.github/workflows/governance-reusable.yml +metadatastician/standards/.github/workflows/hypatia-scan-reusable.yml +metadatastician/standards/.github/workflows/mirror-reusable.yml +metadatastician/standards/.github/workflows/readme-derive-reusable.yml +metadatastician/standards/.github/workflows/rsr-antipattern-reusable.yml +metadatastician/standards/.github/workflows/rust-ci-reusable.yml +metadatastician/standards/.github/workflows/scorecard-reusable.yml +metadatastician/standards/.github/workflows/secret-scanner-reusable.yml +metadatastician/standards/.github/workflows/spark-theatre-gate.yml mozilla-actions/sccache-action oven-sh/setup-bun peter-evans/dockerhub-description diff --git a/scripts/ci-health/remediate.sh b/scripts/ci-health/remediate.sh index 6e01e5bf..38c125f2 100755 --- a/scripts/ci-health/remediate.sh +++ b/scripts/ci-health/remediate.sh @@ -32,7 +32,7 @@ read -r isfork isarch <<<"$meta" case "$CLASS" in B-ALLOWLIST) # Build body: hyperpolymath/* + each superset action as owner/repo@* - body=$(jq -R -s -c 'split("\n") | map(select(length > 0) | . + "@*") | ["hyperpolymath/*"] + . as $pats | {"github_owned_allowed":true,"verified_allowed":true,"patterns_allowed":$pats}' "$HERE/action-superset.txt") + body=$(jq -R -s -c 'split("\n") | map(select(length > 0) | . + "@*") | (["hyperpolymath/*"] + .) as $pats | {"github_owned_allowed":true,"verified_allowed":true,"patterns_allowed":$pats}' "$HERE/action-superset.txt") if [ "$DRY" = "true" ]; then echo "DRYRUN $R/B-ALLOWLIST would PUT $(printf '%s' "$body" | jq '.patterns_allowed | length') patterns"; exit 0; fi printf '%s' "$body" | gh api -X PUT "repos/$O/$R/actions/permissions/selected-actions" --input - >/dev/null expected=$(printf '%s' "$body" | jq -c '.patterns_allowed | sort') diff --git a/test/merge_orchestration/sensor_test.exs b/test/merge_orchestration/sensor_test.exs index b23c48e6..ed6fbc4c 100644 --- a/test/merge_orchestration/sensor_test.exs +++ b/test/merge_orchestration/sensor_test.exs @@ -65,6 +65,12 @@ defmodule Hypatia.MergeOrchestration.SensorTest do }) end + test "standards contractiles stay meta under the repository's current name" do + for repo <- ["metadatastician/standards", "Metadatastician/Standards"] do + assert {_, :meta, _} = Sensor.classify(%{"repo" => repo, "files" => ["rulesets/main.json"]}) + end + end + test "ordinary library code is object-level" do assert {_, :object, _} = Sensor.classify(obs(%{"files" => ["lib/x.ex", "lib/y.ex"]})) end diff --git a/test/rules/sha_bump_propagation_test.exs b/test/rules/sha_bump_propagation_test.exs index 60113480..9b33fe1a 100644 --- a/test/rules/sha_bump_propagation_test.exs +++ b/test/rules/sha_bump_propagation_test.exs @@ -156,11 +156,31 @@ defmodule Hypatia.Rules.ShaBumpPropagationTest do end end + describe "check/1 — the standards repository under its current name" do + test "fires on a merge event from metadatastician/standards" do + [finding] = ShaBumpPropagation.check(event(%{source_repo: "metadatastician/standards"})) + + assert finding.source_repo == "metadatastician/standards" + assert finding.source_workflow == ".github/workflows/governance-reusable.yml" + end + + test "every standards reusable is registered under both names" do + registered = ShaBumpPropagation.known_reusables() + + for {"hyperpolymath/standards", path} <- registered do + assert {"metadatastician/standards", path} in registered + end + end + end + describe "known_reusables/0 — registry hygiene" do test "registry entries are well-formed" do for {repo, path} <- ShaBumpPropagation.known_reusables() do assert is_binary(repo) - assert String.starts_with?(repo, "hyperpolymath/") + + assert String.starts_with?(repo, "hyperpolymath/") or + String.starts_with?(repo, "metadatastician/") + assert String.ends_with?(path, ".yml") or String.ends_with?(path, ".yaml") end end diff --git a/test/rules/standards_repo_test.exs b/test/rules/standards_repo_test.exs new file mode 100644 index 00000000..c9088ecb --- /dev/null +++ b/test/rules/standards_repo_test.exs @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) Jonathan D.A. Jewell +defmodule Hypatia.Rules.StandardsRepoTest do + use ExUnit.Case, async: true + + alias Hypatia.Rules.StandardsRepo + + test "the current name comes first, the former name stays known" do + assert StandardsRepo.current() == "metadatastician/standards" + assert StandardsRepo.names() == ["metadatastician/standards", "hyperpolymath/standards"] + end + + test "name?/1 accepts either name in any case, and nothing else" do + assert StandardsRepo.name?("metadatastician/standards") + assert StandardsRepo.name?("HyperPolymath/Standards") + refute StandardsRepo.name?("hyperpolymath/standards-mirror") + refute StandardsRepo.name?("hyperpolymath/hypatia") + refute StandardsRepo.name?(nil) + end + + test "path_regex_source/1 matches a standards path under either name" do + re = + Regex.compile!( + "^" <> + StandardsRepo.path_regex_source( + "hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml" + ) <> "$" + ) + + assert Regex.match?(re, "hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml") + assert Regex.match?(re, "metadatastician/standards/.github/workflows/rust-ci-reusable.yml") + assert Regex.match?(re, "Metadatastician/Standards/.github/workflows/rust-ci-reusable.yml") + refute Regex.match?(re, "metadatastician/standards/.github/workflows/rust-ciXreusable.yml") + refute Regex.match?(re, "someone/standards/.github/workflows/rust-ci-reusable.yml") + end + + test "path_regex_source/1 escapes any other path as it stands" do + assert StandardsRepo.path_regex_source( + "hyperpolymath/cicd-suite/.github/workflows/rust-ci.yml" + ) == + Regex.escape("hyperpolymath/cicd-suite/.github/workflows/rust-ci.yml") + end +end diff --git a/test/rules/workflow_audit_pins_test.exs b/test/rules/workflow_audit_pins_test.exs index 7cdfaabb..fbaa2590 100644 --- a/test/rules/workflow_audit_pins_test.exs +++ b/test/rules/workflow_audit_pins_test.exs @@ -1117,15 +1117,13 @@ defmodule Hypatia.Rules.WorkflowAuditPinsTest do end test "an alias name runs to whitespace or a flow indicator" do - wf = - "x: &e/x \"\"\n" <> @rust_step <> " with:\n toolchain: *e/x\n" + wf = "x: &e/x \"\"\n" <> @rust_step <> " with:\n toolchain: *e/x\n" assert [%{line: 5, auto_fixable: true}] = wf026(wf) end test "an anchor directly after a quoted key's colon in a flow mapping is the anchor" do - wf = - "x: {\"e\":&e \"\"}\n" <> @rust_step <> " with:\n toolchain: *e\n" + wf = "x: {\"e\":&e \"\"}\n" <> @rust_step <> " with:\n toolchain: *e\n" # The anchor is read, so the empty value it holds routes to the fixer # rather than to review as an alias of no anchor. @@ -1335,6 +1333,23 @@ defmodule Hypatia.Rules.WorkflowAuditPinsTest do assert f.reason =~ "standards#439" end + test "fires on the same call under the repository's current name" do + new_name = String.replace_prefix(@rust_ci, "hyperpolymath/", "metadatastician/") + wf = "jobs:\n rust-ci:\n uses: #{new_name}@#{@broken_standards}\n" + [f] = WorkflowAudit.check_known_broken_reusable_revision(%{"rust-ci.yml" => wf}) + assert f.rule == "WF027" + assert f.current_sha == @broken_standards + end + + test "WF016 fires on an orphan pin under the repository's current name" do + wf = + "jobs:\n scan:\n uses: metadatastician/standards/.github/workflows/" <> + "hypatia-scan-reusable.yml@97df76210d966e0e1a89d5e5e9bcf66e16cea5e8\n" + + assert [%{rule: "WF016"}] = + WorkflowAudit.check_orphan_standards_reusable_pin(%{"scan.yml" => wf}) + end + test "silent at the replacement revision" do wf = "jobs:\n rust-ci:\n uses: #{@rust_ci}@#{@good_standards}\n" assert WorkflowAudit.check_known_broken_reusable_revision(%{"rust-ci.yml" => wf}) == [] diff --git a/test/security_errors_test.exs b/test/security_errors_test.exs index 3b90d561..9d8324d8 100644 --- a/test/security_errors_test.exs +++ b/test/security_errors_test.exs @@ -78,6 +78,14 @@ defmodule Hypatia.Rules.SecurityErrorsTest do assert SecurityErrors.pin_exemption_reason(ref) =~ "provenance" end + test "own-org standards reusables are exempt under either name of the repository" do + for owner <- ["hyperpolymath", "metadatastician"] do + ref = "#{owner}/standards/.github/workflows/governance-reusable.yml@main" + assert SecurityErrors.pin_exempt?(ref) + assert SecurityErrors.pin_exemption_reason(ref) =~ "wrapper pattern" + end + end + test "pin_action returns :exempt (never a SHA fix) for exempt actions" do assert {:exempt, reason} = SecurityErrors.pin_action("slsa-framework/slsa-github-generator@v2.1.0")