From d84e18a48953d435eb224cadb7e47f8d5bf611ea Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:43:29 +0100 Subject: [PATCH] fix(governance): green the Validate Hypatia Baseline gate on main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `governance / Validate Hypatia Baseline` has been the only real red on main (run 35783374301, job 106934294699, 38s -- a real verdict, not a startup death): "Gate failed: 5 unfiltered finding(s) at or above 'info'". A standing red on main is itself a detector-blinding condition -- while it sits there nobody can distinguish a new gate failure from the old one. Five findings, two clusters, two different causes. 1. Three security_errors/secret_detected (critical) at lib/hypatia/scanner_suppression.ex:252. The comment documenting the three @secret_patterns that match on form alone spells out the three shapes -- and is therefore matched by all three of the patterns it documents. Same family as the RE005 defect in #834: documenting a false-positive class creates instances of it. Cured with a line-scoped inline directive. Not a baseline entry: the baseline match key has no `line` field, so an entry would suppress every present and future secret finding in this file. Not @training_corpus_paths either -- that is compiled into the scanner, which the gate builds from a pin, so it cannot affect this gate at all, and it would blind the scanner core to a real leak. 2. Two code_safety findings (high) on ffi/zig/src/main.zig -- zig_ptr_cast and zig_align_cast on the mandatory opaque-handle idiom. Already filed as #834. Cured with two .hypatia-baseline.json acknowledgements carrying tracking_issue: hyperpolymath/hypatia#834. The baseline is the only mechanism that can reach these: the rule reports at main.zig:1 rather than the cast site, so no per-line directive applies, and the only other source-level option is file-wide. These entries are to be DELETED when #834 lands -- #834's own acceptance criteria require main.zig clean without changing the cast. Root cause of cluster 1, for the record: the gate builds the scanner from 0e913426 (2026-09-06) and scans HEAD. bc8812e -- label-aware secret suppression, #782, 2026-09-14 -- is an ancestor of main and is NOT an ancestor of the pin. Our own cure for this class is in the repo and absent from the scanner enforcing the gate. Bumping the pin is a required check on ~120 caller repos, so it is filed separately rather than folded in here. Verified by local reproduction of CI's three steps (scanner built from the pin, apply-baseline.sh at its own sparse-checkout pin 874ffe58, BLOCKING_THRESHOLD info): control 6 raw / 5 kept / 1 suppressed, matching CI field-for-field; cured 3 raw / 0 kept / 3 suppressed. Five mutants, all killed: - ghp_ + 36 chars in lib/hypatia/cli.ex -> red (kept 1) - ghp_ + 36 chars elsewhere in this file -> red (kept 1); the allow is LINE-scoped, not file-scoped - delete the two zig baseline entries -> red (kept 2) - delete the inline directive line -> red (kept 3), regenerating the original failure exactly - revert all -> green (kept 0) Accepted trade-off, stated rather than left to be found: a real secret placed on the one directive-covered line would be suppressed. That line is a fixed documentation literal listing three regex shapes; any edit to it is visible in review, and every other line and file still fails the gate. The 45 pre-existing baseline entries are byte-identical (16 insertions, 0 deletions) -- the entries were spliced textually. Rewriting the file through jq silently re-encodes \u2014 escape sequences as literal em dashes in entries it was never asked to touch, which would have shown up as a spurious deletion. Ratchet-exception: .hypatia-baseline.json — two acknowledgements for the opaque-handle false positive tracked in hyperpolymath/hypatia#834. The ledger grows 45 -> 47 and the exemption ratchet is right to ask. The baseline is the only mechanism that can reach a finding the rule reports at main.zig:1, and #834's acceptance criteria require deleting both entries when it lands. Both carry a note and a tracking_issue, so neither is anonymous debt. Refs #855, #834, #782 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0113HQM9LVGkNCzU1WwkJZSV --- .hypatia-baseline.json | 16 ++++++++++++++++ lib/hypatia/scanner_suppression.ex | 11 ++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.hypatia-baseline.json b/.hypatia-baseline.json index 456ae421..9d8fc9d1 100644 --- a/.hypatia-baseline.json +++ b/.hypatia-baseline.json @@ -269,5 +269,21 @@ "type": "DependencyPinning", "file": ".", "note": "Ingested from OSSF Scorecard's Pinned-Dependencies check, which inspects workflows for inline SHA refs and has no knowledge of GitHub Actions lockfiles. This repository pins every action in .github/workflows/actions.lock, which resolves each symbolic ref to a verified commit plus the transitive dependencies of composite actions. Inline SHA-pinning to satisfy Scorecard would REMOVE actions from the lockfile (gh actions-lock rejects refs no tag or branch contains) and reduce coverage \u2014 measured 2026-08-07: it put 14 workflows into startup_failure. Acknowledged as an external tool limitation, not accepted debt." + }, + { + "severity": "high", + "rule_module": "code_safety", + "type": "zig_ptr_cast", + "file": "ffi/zig/src/main.zig", + "note": "The mandatory opaque-handle idiom, not an unchecked conversion. ffi/zig/src/main.zig declares `pub const Handle = opaque {}` so the concrete `HandleState` is never named in the C header; recovering it from the opaque pointer requires exactly `@ptrCast(@alignCast(handle))` (line 60) and `@ptrCast(handle)` (line 84). There is no safe alternative -- the opaque handle is the point, and the six normative ABI functions in src/Hypatia/ABI/FFI.idr depend on it. The rule matches a bare ~r/@ptrCast/ at :high (CWE-704), strips no Zig comments, and reports the finding at main.zig:1 rather than the cast site, so no per-line inline directive can reach it. Acknowledged as a rule defect tracked in hyperpolymath/hypatia#834, not accepted debt: that issue's acceptance criteria require main.zig to be clean WITHOUT changing the cast, and this entry is to be deleted when it lands.", + "tracking_issue": "hyperpolymath/hypatia#834" + }, + { + "severity": "high", + "rule_module": "code_safety", + "type": "zig_align_cast", + "file": "ffi/zig/src/main.zig", + "note": "Same site and same cause as the zig_ptr_cast entry above: `@alignCast` is the inner half of `@ptrCast(@alignCast(handle))` at ffi/zig/src/main.zig:60, the required way to recover `*HandleState` from an opaque `*Handle`. Tracked in hyperpolymath/hypatia#834; delete this entry when the rule is fixed.", + "tracking_issue": "hyperpolymath/hypatia#834" } ] diff --git a/lib/hypatia/scanner_suppression.ex b/lib/hypatia/scanner_suppression.ex index ce2b8530..60f5bc44 100644 --- a/lib/hypatia/scanner_suppression.ex +++ b/lib/hypatia/scanner_suppression.ex @@ -249,7 +249,16 @@ defmodule Hypatia.ScannerSuppression do # ── Comment-masked generic secrets ──────────────────────────────────────── # # Three of the 18 `@secret_patterns` in `Hypatia.Rules.SecurityErrors` match - # on FORM ALONE — `api_key = "..."`, `secret = "..."`, `password = "..."`. + # on FORM ALONE. Spelling those three shapes out is what makes this comment + # useful — and it is also, unavoidably, three matches for the very patterns + # being described. That is why the line below carries a directive. It is + # scoped to that ONE line: a real credential anywhere else in this file + # still fails the gate, which a file-level or baseline suppression would + # not guarantee. + # + # hypatia: allow security_errors/secret_detected -- documentation example + # `api_key = "..."`, `secret = "..."`, `password = "..."` + # # Any prose example, changelog entry or commented-out config line carrying # that shape is indistinguishable from a real leak, and commented-out # examples are the entire measured false-positive population.