Skip to content

Commit 614451b

Browse files
ci: fix the last two main-red roots — Hypatia startup_failure (#451, this repo only) + scorecard-verify false-red (new) (#514)
Completes the run to green on `main`, after #511 (secret-scanner caller + SPDX) and #512 (gitleaks allowlist). Two remaining causes, independent of each other and of those PRs. ## 1. Hypatia has never run — the caller under-grants (#451) `hypatia-scan-reusable.yml` declares `security-events: write` (it uploads SARIF). `hypatia-scan.yml` granted `security-events: **read**`. A called workflow may never request more than its caller grants. When it does, GitHub rejects the run at **startup** — before any job exists: ``` completed startup_failure Hypatia Security Scan main push 1s $ gh run view 29764318317 --log-failed failed to get run log: log not found # <- there is no log; no job ran ``` Granting `read` did not degrade the scan to a no-upload mode. **It stopped the scan from existing.** The reusable carried this comment: ```yaml continue-on-error: true # callers granting only security-events:read skip gracefully ``` That is a false mental model, and it is load-bearing — it is the reason the mismatch looked intentional. Nothing skips gracefully in a job that was never created. Corrected in place. This is exactly the #451 pattern that startup-fails reusable-workflow callers estate-wide. **Fixed here on the canon's own caller only** — deliberately demonstrated on one repo before any propagation is proposed. ## 2. Seven "fake passes" that were every one of them real (#381) `registry-verify.yml` runs `build-scorecards.sh --verify`, which **executes** every pass-row's check. The job installed no tools, and `ubuntu-latest` ships neither ripgrep nor xmllint: | rows | mechanism | observed | |---|---|---| | `k9-svc/M6` | calls `xmllint` directly | exit **127** | | 6 × `release-pre-flight/*` | invoke `v1-audit.sh`, which hard-exits 2 without ripgrep, so the greps match nothing | exit **1** | The verifier reported all seven as **"claimed PASS but the pass is not real"**. Reproduced locally by stripping `rg` from `PATH` — identical output. Measured with the tools present: ``` ── verify: 87 grounded pass · 0 broken pass · 0 unrunnable · 0 self-asserted · 0 stale-fail EXIT=0 ``` **All seven claims were true.** The checks had never run. A false-red is not harmless: it trains maintainers to ignore a gate — the same end state as the fake-green gates #500 removed, reached from the opposite direction. Two changes, since installing the tools alone leaves the trap armed for the next missing dependency: - `registry-verify.yml` installs `ripgrep` + `libxml2-utils`. - `run_verify()` **refuses to judge pass-rows at all** in an incomplete environment, and reports genuinely unrunnable checks as `unrunnable` — still failing, since an unverifiable pass must never go green — rather than as false claims. ### The required-tool list is declared, not inferred An earlier draft parsed check strings to discover their commands. It misparsed quoted regexes (`"a\|b\|c"`) and `[ $(…) -ge 25 ]`, inventing **35** failures — precisely the sin the guard exists to prevent. Replaced with a measured, documented list: `xmllint` is named by 4 checks, `jq` by 1, and `rg` by none but is required transitively via `v1-audit.sh`. ## Verification - complete env → `87 grounded · 0 broken · 0 unrunnable`, exit 0 - incomplete env → fails fast, judges nothing, names the missing tool - `k9-svc/mime/k9.xml` + `k9.uti.plist` independently confirmed well-formed and the two companion files present — so installing xmllint turns that row **genuinely** green rather than exposing a real fault - all three workflow files parse ## Not included, on purpose `.gitleaks.toml` — **#512 covers it and is the better implementation.** I built one independently and then discarded it: my regexes matched against the whole line unanchored, and canary-testing showed that suppressed real tokens. | planted secret | gitleaks defaults | my draft | |---|---|---| | `ghp_…` PAT | caught | caught | | same PAT, line also contains `EXAMPLE_KEY` | caught | **suppressed** | | same PAT, line ends `# ...` | caught | **suppressed** | #512's anchored `^…$` design has no such hole. Verified independently against gitleaks 8.18.4 (the exact CI build, checksum-matched): all four adversarial fixtures still caught, and `0` findings on the tree. Adopt #512. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f3b56e0 commit 614451b

4 files changed

Lines changed: 117 additions & 4 deletions

File tree

.github/workflows/hypatia-scan-reusable.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,14 @@ jobs:
123123
124124
- name: Upload SARIF to code scanning
125125
if: always()
126-
continue-on-error: true # callers granting only security-events:read skip gracefully
126+
# NOTE: this does NOT make under-permissioned callers "skip gracefully".
127+
# This workflow declares `security-events: write` at the top, so a
128+
# caller granting only `read` is rejected at startup and NO step of
129+
# this job ever executes — `continue-on-error` cannot rescue a job
130+
# that was never created. Callers MUST grant `security-events: write`.
131+
# This flag only tolerates a genuine upload failure (e.g. Advanced
132+
# Security disabled on a private repo) once the job is actually running.
133+
continue-on-error: true
127134
uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v3
128135
with:
129136
sarif_file: hypatia.sarif

.github/workflows/hypatia-scan.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ on:
1212

1313
permissions:
1414
contents: read
15-
security-events: read
15+
# MUST be `write`, not `read`. hypatia-scan-reusable.yml declares
16+
# `security-events: write` so it can upload SARIF. A called workflow may
17+
# never request more than its caller grants: if it does, GitHub rejects the
18+
# run at *startup*, before any job is created — `startup_failure`, zero jobs,
19+
# and `gh run view --log-failed` returns "log not found". Granting `read`
20+
# here did not degrade the scan, it silently prevented it from ever running.
21+
# See standards#451.
22+
security-events: write
1623

1724
jobs:
1825
scan:

.github/workflows/registry-verify.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,26 @@ jobs:
2929
with:
3030
fetch-depth: 0 # full history so `git ls-files -s` hashes are stable
3131

32+
# The scorecard `--verify` step below EXECUTES every pass-row's check.
33+
# Those checks shell out to real tools; ubuntu-latest ships neither
34+
# ripgrep nor xmllint. Without them `release-pre-flight/v1-audit.sh`
35+
# exits 2 ("v1-audit requires ripgrep") and the k9-svc MIME check exits
36+
# 127 — which the verifier then reported as "claimed PASS but the pass
37+
# is not real". That was a FALSE ACCUSATION: the passes were real, the
38+
# checks simply never ran. A gate that cannot run its own checks must
39+
# not be allowed to call the repo dishonest.
40+
#
41+
# NB this is a distinct fault from standards#381, which concerns
42+
# REGISTRY.a2ml source_hash drift in the *first* step below and is
43+
# not addressed here.
44+
- name: Install check dependencies (ripgrep, xmllint)
45+
run: |
46+
set -euo pipefail
47+
sudo apt-get update -qq
48+
sudo apt-get install -y --no-install-recommends ripgrep libxml2-utils
49+
rg --version | head -1
50+
xmllint --version 2>&1 | head -1
51+
3252
- name: Verify registry + derived topology are current
3353
run: |
3454
if ! bash scripts/build-registry.sh --check; then

scripts/build-scorecards.sh

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,68 @@ extract_checks() {
142142
# * pass + no check -> self-asserted (reported; visible debt)
143143
# * fail/other + check exits 0 -> stale-fail (advisory: re-grade candidate)
144144
# ---------------------------------------------------------------------------
145+
# Tools the checks shell out to that are NOT present on a bare runner.
146+
#
147+
# Why this exists: a check whose tool is absent is indistinguishable from a
148+
# check that ran and failed, so the verifier accused the repo of claiming a
149+
# fake pass. `xmllint` missing surfaced as exit 127; ripgrep missing made
150+
# release-pre-flight/v1-audit.sh exit 2, so its greps matched nothing and
151+
# returned 1. All seven were reported as "the pass is not real" — and all
152+
# seven passes were in fact real.
153+
#
154+
# Newly diagnosed; this is NOT standards#381. That issue is REGISTRY.a2ml
155+
# source_hash drift in build-registry.sh — the *first* step of the same CI
156+
# job, a different script and a different mechanism, still unaddressed.
157+
#
158+
# Declared explicitly rather than inferred: splitting a check string to guess
159+
# its commands is unreliable (a quoted regex like "a\|b\|c", or `[ $(…) -ge
160+
# 25 ]`, both misparse) and a mis-parse would invent failures — the very sin
161+
# this guard exists to prevent.
162+
#
163+
# Add a tool here only if a check genuinely needs it AND it is absent from a
164+
# default ubuntu-latest image. CI installs these in registry-verify.yml.
165+
# Measured against the current scorecards: `xmllint` is named by 4 checks and
166+
# `jq` by 1; `rg` is named by none but is required *transitively* — the six
167+
# release-pre-flight checks invoke v1-audit.sh, which hard-exits 2 without it.
168+
# (`python3` and `cargo` are also referenced but ship on ubuntu-latest.)
169+
VERIFY_OPTIONAL_TOOLS="rg xmllint jq"
170+
171+
check_missing_tools() {
172+
local chk="$1" t out=""
173+
for t in $VERIFY_OPTIONAL_TOOLS; do
174+
# Word-boundary containment test, no tokenising: only reports a tool that
175+
# the check actually names and that is actually not installed.
176+
case " $chk " in
177+
*[!A-Za-z0-9_-]"$t"[!A-Za-z0-9_-]*)
178+
command -v "$t" >/dev/null 2>&1 || out="$out$t " ;;
179+
esac
180+
done
181+
printf '%s' "$out"
182+
}
183+
145184
run_verify() {
146-
local rc=0 grounded=0 selfasserted=0 broken=0 stale=0 f base id status chk crc
185+
local rc=0 grounded=0 selfasserted=0 broken=0 stale=0 unrunnable=0
186+
local f base id status chk crc miss t absent=""
147187
echo "== scorecard --verify: running pass-row checks =="
188+
189+
# ---- environment preflight -------------------------------------------
190+
# Per-check tool detection cannot be complete: a check may invoke a repo
191+
# script that needs a tool the check string never names. release-pre-flight
192+
# checks call v1-audit.sh, which exits 2 when ripgrep is missing; its greps
193+
# then match nothing and the verifier concluded SIX claimed passes were
194+
# "not real". They were real. Rather than risk that class of false
195+
# accusation, refuse to verify at all in an incomplete environment.
196+
for t in $VERIFY_OPTIONAL_TOOLS; do
197+
command -v "$t" >/dev/null 2>&1 || absent="$absent$t "
198+
done
199+
if [ -n "$absent" ]; then
200+
echo "⛔ verify environment incomplete — missing: ${absent% }" >&2
201+
echo " Checks (and the scripts they invoke) could fail for lack of" >&2
202+
echo " tooling rather than because a claim is false, so pass-rows will" >&2
203+
echo " NOT be judged. Install the tools and re-run:" >&2
204+
echo " sudo apt-get install -y ripgrep libxml2-utils jq" >&2
205+
return 1
206+
fi
148207
shopt -s nullglob
149208
for f in "$SCDIR"/*.scorecard.a2ml; do
150209
base="$(basename "$f" .scorecard.a2ml)"
@@ -155,11 +214,26 @@ run_verify() {
155214
selfasserted=$((selfasserted + p_total - p_chk))
156215
while IFS=$'\x1f' read -r id status chk; do
157216
[ -z "$chk" ] && continue
217+
# A check whose tooling is absent did not run, so it can neither ground
218+
# nor refute the claimed status. Report it as unrunnable — still a
219+
# failure (an unverifiable pass must never go green), but described
220+
# accurately so the fix is "install the tool", not "the claim is a lie".
221+
miss="$(check_missing_tools "$chk")"
222+
if [ -n "$miss" ]; then
223+
echo "$base/$id: check CANNOT RUN — missing tool(s): ${miss% }"
224+
echo " check: $chk"
225+
unrunnable=$((unrunnable + 1)); rc=1
226+
continue
227+
fi
158228
# Checks run read-only from the repo root; they must not mutate.
159229
( cd "$(git rev-parse --show-toplevel)" && bash -c "$chk" ) >/dev/null 2>&1; crc=$?
160230
if [ "$status" = "pass" ]; then
161231
if [ "$crc" -eq 0 ]; then
162232
grounded=$((grounded + 1))
233+
elif [ "$crc" -eq 127 ]; then
234+
echo "$base/$id: check CANNOT RUN — command not found (exit 127)"
235+
echo " check: $chk"
236+
unrunnable=$((unrunnable + 1)); rc=1
163237
else
164238
echo "$base/$id: claimed PASS but check exited $crc — the pass is not real"
165239
echo " check: $chk"
@@ -174,10 +248,15 @@ run_verify() {
174248
done < <(extract_checks "$f")
175249
done
176250
shopt -u nullglob
177-
echo " ── verify: $grounded grounded pass · $broken broken pass · $selfasserted self-asserted pass · $stale stale-fail"
251+
echo " ── verify: $grounded grounded pass · $broken broken pass · $unrunnable unrunnable · $selfasserted self-asserted pass · $stale stale-fail"
178252
if [ "$broken" -gt 0 ]; then
179253
echo "❌ --verify FAILED: $broken claimed pass(es) whose check does not hold" >&2
180254
fi
255+
if [ "$unrunnable" -gt 0 ]; then
256+
echo "⛔ --verify FAILED: $unrunnable check(s) could not run (missing tooling)." >&2
257+
echo " These are NOT fake passes — the checks never executed. Install the" >&2
258+
echo " listed tools and re-run. CI installs them in registry-verify.yml." >&2
259+
fi
181260
return $rc
182261
}
183262

0 commit comments

Comments
 (0)