Skip to content

Commit 9b0875f

Browse files
feat(rules): consolidate two parallel SHA-pin databases into one canonical source (#385)
## Summary Audit 2026-05-28 Part 3.7 flagged the two-database setup as a maintenance hazard. Consolidating: | Before | Entries | Unique-to-it | |---|---|---| | `SecurityErrors.@sha_pins` | 17 | `ocaml/setup-ocaml@v3`, `softprops/action-gh-release@v2` | | `WorkflowAudit.@known_good_shas` | 19 | `github/codeql-action@v4`, `denoland/setup-deno@v2`, 3× `hyperpolymath/*` | | After | Entries | Notes | |---|---|---| | `SecurityErrors.@sha_pins` | **22** (union) | canonical | | `WorkflowAudit.known_good_shas/0` | delegates | no longer carries its own map | In-file callers updated: `WorkflowAudit.check_unpinned_actions/1` + `WorkflowAudit.check_wrong_pins/1` now call `SecurityErrors.sha_pins/0`. `slsa-framework/slsa-github-generator` is still deliberately omitted (pin-exempt per `SecurityErrors.@pin_exempt` and hypatia#262). ## Test plan - [x] 5 cases in `test/rules/sha_pin_consolidation_test.exs`: delegation equality, entries-formerly-unique-to-each-place survive, shared entries unchanged, total ≥ 22 - [x] 5/5 pass via standalone elixir - [ ] Full `mix test` green in CI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2a2198d commit 9b0875f

3 files changed

Lines changed: 80 additions & 32 deletions

File tree

lib/rules/security_errors.ex

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,23 @@ defmodule Hypatia.Rules.SecurityErrors do
270270
# GitHub Action SHA Pins (canonical)
271271
# ---------------------------------------------------------------------------
272272

273+
# CANONICAL SHA-PIN DATABASE for the estate.
274+
#
275+
# Audit 2026-05-28 Part 3.7 flagged the previous two-database setup
276+
# (this @sha_pins plus workflow_audit/@known_good_shas) as a
277+
# maintenance hazard: each update had to touch both maps in lockstep,
278+
# and they had drifted to different memberships (workflow_audit
279+
# carried codeql-action@v4, denoland/setup-deno@v2, the three
280+
# hyperpolymath/* entries; this map carried ocaml/setup-ocaml,
281+
# softprops/action-gh-release). Consolidated into the union here.
282+
# workflow_audit/known_good_shas now delegates to this map.
273283
@sha_pins %{
274284
"actions/checkout@v4" => "34e114876b0b11c390a56381ad16ebd13914f8d5",
275285
"actions/checkout@v5" => "93cb6efe18208431cddfb8368fd83d5badbf9bfd",
276286
"github/codeql-action@v3" => "6624720a57d4c312633c7b953db2f2da5bcb4c3a",
287+
"github/codeql-action@v4" => "d4b3ca9fa7f69d38bfcd667bdc45bc373d16277e",
277288
"ossf/scorecard-action@v2.4.0" => "62b2cac7ed8198b15735ed49ab1e5cf35480ba46",
289+
"denoland/setup-deno@v2" => "909cc5acb0fdd60627fb858598759246509fa755",
278290
"dtolnay/rust-toolchain@stable" => "4be9e76fd7c4901c61fb841f559994984270fce7",
279291
"Swatinem/rust-cache@v2" => "779680da715d629ac1d338a641029a2f4372abb5",
280292
"codecov/codecov-action@v5" => "671740ac38dd9b0130fbe1cec585b89eea48d3de",
@@ -287,7 +299,10 @@ defmodule Hypatia.Rules.SecurityErrors do
287299
"actions/upload-pages-artifact@v3" => "56afc609e74202658d3ffba0e8f6dda462b719fa",
288300
"actions/deploy-pages@v4" => "d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e",
289301
"ruby/setup-ruby@v1" => "09a7688d3b55cf0e976497ff046b70949eeaccfd",
290-
"editorconfig-checker/action-editorconfig-checker@main" => "4054fa83a075fdf090bd098bdb1c09aaf64a4169"
302+
"editorconfig-checker/action-editorconfig-checker@main" => "4054fa83a075fdf090bd098bdb1c09aaf64a4169",
303+
"hyperpolymath/a2ml-validate-action@main" => "cb3c1e298169dc5ac2b42e257068b0fb5920cd5e",
304+
"hyperpolymath/k9-validate-action@main" => "236f0035cc159051c8dd5dc7cd8af1e8cf961462",
305+
"hyperpolymath/panic-attacker/.github/workflows/scan-and-report.yml@main" => "21fc3f00a088c954912936f4a68970621b82c2e6"
291306
}
292307

293308
def sha_pins, do: @sha_pins

lib/rules/workflow_audit.ex

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -39,37 +39,21 @@ defmodule Hypatia.Rules.WorkflowAudit do
3939
"secret-scanner.yml"
4040
]
4141

42-
# ─── Known-good SHA pins (2026-02-04 baseline) ────────────────────────
43-
44-
@known_good_shas %{
45-
"actions/checkout@v4" => "34e114876b0b11c390a56381ad16ebd13914f8d5",
46-
"actions/checkout@v5" => "93cb6efe18208431cddfb8368fd83d5badbf9bfd",
47-
"github/codeql-action@v3" => "6624720a57d4c312633c7b953db2f2da5bcb4c3a",
48-
"github/codeql-action@v4" => "d4b3ca9fa7f69d38bfcd667bdc45bc373d16277e",
49-
"denoland/setup-deno@v2" => "909cc5acb0fdd60627fb858598759246509fa755",
50-
"ossf/scorecard-action@v2.4.0" => "62b2cac7ed8198b15735ed49ab1e5cf35480ba46",
51-
"trufflesecurity/trufflehog@main" => "7ee2e0fdffec27d19ccbb8fb3dcf8a83b9d7f9e8",
52-
"dtolnay/rust-toolchain@stable" => "4be9e76fd7c4901c61fb841f559994984270fce7",
53-
"Swatinem/rust-cache@v2" => "779680da715d629ac1d338a641029a2f4372abb5",
54-
"codecov/codecov-action@v5" => "671740ac38dd9b0130fbe1cec585b89eea48d3de",
55-
"editorconfig-checker/action-editorconfig-checker@main" => "4054fa83a075fdf090bd098bdb1c09aaf64a4169",
56-
# NOTE: slsa-framework/slsa-github-generator deliberately removed — it is
57-
# pin-exempt (self-verifies github.ref). See SecurityErrors.@pin_exempt
58-
# and hyperpolymath/hypatia#262. SHA-pinning it breaks SLSA provenance.
59-
"webfactory/ssh-agent@v0.9.0" => "dc588b651fe13675774614f8e6a936a468676387",
60-
"actions/configure-pages@v5" => "983d7736d9b0ae728b81ab479565c72886d7745b",
61-
"actions/jekyll-build-pages@v1" => "44a6e6beabd48582f863aeeb6cb2151cc1716697",
62-
"actions/upload-pages-artifact@v3" => "56afc609e74202658d3ffba0e8f6dda462b719fa",
63-
"actions/deploy-pages@v4" => "d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e",
64-
"ruby/setup-ruby@v1" => "09a7688d3b55cf0e976497ff046b70949eeaccfd",
65-
"hyperpolymath/a2ml-validate-action@main" => "cb3c1e298169dc5ac2b42e257068b0fb5920cd5e",
66-
"hyperpolymath/k9-validate-action@main" => "236f0035cc159051c8dd5dc7cd8af1e8cf961462",
67-
"hyperpolymath/panic-attacker/.github/workflows/scan-and-report.yml@main" =>
68-
"21fc3f00a088c954912936f4a68970621b82c2e6"
69-
}
42+
# ─── Known-good SHA pins ──────────────────────────────────────────────
43+
#
44+
# Consolidated 2026-05-28 (audit Part 3.7): this module previously
45+
# carried its own @known_good_shas with overlapping but DIFFERENT
46+
# entries from SecurityErrors.@sha_pins. The two maps had drifted to
47+
# different memberships, and every update had to touch both. Now
48+
# delegates to SecurityErrors.sha_pins/0 (the canonical source).
49+
# NOTE: slsa-framework/slsa-github-generator is deliberately omitted —
50+
# it is pin-exempt (self-verifies github.ref). See
51+
# SecurityErrors.@pin_exempt and hyperpolymath/hypatia#262. SHA-pinning
52+
# it breaks SLSA provenance.
7053

7154
def standard_workflows, do: @standard_workflows
72-
def known_good_shas, do: @known_good_shas
55+
56+
def known_good_shas, do: Hypatia.Rules.SecurityErrors.sha_pins()
7357

7458
# ─── Audit functions ───────────────────────────────────────────────────
7559

@@ -250,7 +234,7 @@ defmodule Hypatia.Rules.WorkflowAudit do
250234
action_ref: action_ref,
251235
severity: severity,
252236
action: :pin_sha,
253-
known_sha: Map.get(@known_good_shas, action_ref)
237+
known_sha: Map.get(Hypatia.Rules.SecurityErrors.sha_pins(), action_ref)
254238
}
255239
end
256240
end)
@@ -272,7 +256,7 @@ defmodule Hypatia.Rules.WorkflowAudit do
272256
Regex.scan(~r/uses:\s*([^\s#]+)@([0-9a-f]{40})/m, content)
273257
|> Enum.flat_map(fn [_full, action, sha] ->
274258
# Check if we have a known-good SHA for any version of this action
275-
matching = Enum.find(@known_good_shas, fn {ref, _} ->
259+
matching = Enum.find(Hypatia.Rules.SecurityErrors.sha_pins(), fn {ref, _} ->
276260
String.starts_with?(ref, action <> "@")
277261
end)
278262

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
3+
defmodule Hypatia.Rules.ShaPinConsolidationTest do
4+
use ExUnit.Case, async: true
5+
6+
alias Hypatia.Rules.SecurityErrors
7+
alias Hypatia.Rules.WorkflowAudit
8+
9+
# Audit 2026-05-28 Part 3.7: consolidate the two SHA-pin databases into
10+
# one canonical source. SecurityErrors.@sha_pins is the source of truth;
11+
# WorkflowAudit.known_good_shas/0 delegates to it.
12+
13+
describe "SHA-pin database consolidation" do
14+
test "WorkflowAudit.known_good_shas/0 delegates to SecurityErrors.sha_pins/0" do
15+
assert WorkflowAudit.known_good_shas() == SecurityErrors.sha_pins()
16+
end
17+
18+
test "canonical set contains entries previously unique to security_errors" do
19+
pins = SecurityErrors.sha_pins()
20+
assert Map.has_key?(pins, "ocaml/setup-ocaml@v3")
21+
assert Map.has_key?(pins, "softprops/action-gh-release@v2")
22+
end
23+
24+
test "canonical set contains entries previously unique to workflow_audit" do
25+
pins = SecurityErrors.sha_pins()
26+
assert Map.has_key?(pins, "github/codeql-action@v4")
27+
assert Map.has_key?(pins, "denoland/setup-deno@v2")
28+
assert Map.has_key?(pins, "hyperpolymath/a2ml-validate-action@main")
29+
assert Map.has_key?(pins, "hyperpolymath/k9-validate-action@main")
30+
assert Map.has_key?(pins, "hyperpolymath/panic-attacker/.github/workflows/scan-and-report.yml@main")
31+
end
32+
33+
test "shared entries (in both old maps) match the same SHA" do
34+
pins = SecurityErrors.sha_pins()
35+
# actions/checkout@v4 was in both — make sure it survived consolidation
36+
assert pins["actions/checkout@v4"] == "34e114876b0b11c390a56381ad16ebd13914f8d5"
37+
# github/codeql-action@v3 was in both
38+
assert pins["github/codeql-action@v3"] == "6624720a57d4c312633c7b953db2f2da5bcb4c3a"
39+
end
40+
41+
test "no entries lost in consolidation — at least 22 pins total" do
42+
# security_errors had 17 entries before; workflow_audit had 19.
43+
# Union = 17 + 19 - intersection (15 shared) = 21.
44+
# Adding the WH004-only github/codeql-action@v4 etc, expecting >= 22.
45+
pins = SecurityErrors.sha_pins()
46+
assert map_size(pins) >= 22, "Expected union to be at least 22 pins; got #{map_size(pins)}"
47+
end
48+
end
49+
end

0 commit comments

Comments
 (0)