From 81de2b0ddd169749437801617bf915fa55cf006a Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" Date: Thu, 15 Oct 2026 18:22:51 +0100 Subject: [PATCH 1/3] feat(cli): live detection for HYP-S010 and HYP-S011 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the two finding classes from the 2026-10-15 CodeRabbit estate sweep into the bash CLI pattern layer (rule lifecycle step 1 — the patterns are active before the Elixir evaluators exist): - scan_clade_identity (HYP-S010): a CLADE.a2ml must declare THIS repository and carry the recomputable profile-C mint for that declaration. Three checks: template residue (canonical-name == rsr-template-repo outside the template repo), uuid not recomputable from the declared owner/name over gv-clade-index, and preimage identifying a different repository than the declaration. - scan_workflows gains uses_ref_not_sha (HYP-S011): every uses: ref must be a full commit SHA; version tags are findings. Comment lines and NOSONAR-suppressed lines are exempt (the estate's pin-authority convention). Verified against the real fixtures: the fixed odraz #19 tree is silent; the original PR state fires template-residue and non-recomputable-uuid; the CodeRabbit autofix state (the wrong direction) fires template-residue. choreographic-types #33 pre-fix fires on exactly codeql.yml and proofs.yml; post-fix is silent. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- hypatia-cli-bash.sh | 78 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/hypatia-cli-bash.sh b/hypatia-cli-bash.sh index 9b2712c8..545fdd64 100755 --- a/hypatia-cli-bash.sh +++ b/hypatia-cli-bash.sh @@ -702,12 +702,89 @@ scan_workflows() { "$wf" 0 "pull_request_target + checkout = code execution from untrusted PRs" "CWE-829" \ "Do not checkout PR code in pull_request_target; use pull_request instead" fi + + # HYP-S011: a uses: ref that is a version tag, not a full commit SHA. + # A SHA ref with the tag kept as a trailing comment does NOT match + # (the @ is followed by hex, and backtracking cannot reach @v). + # Comment lines (documentation examples) and NOSONAR-suppressed lines + # (the estate's pin-authority convention, e.g. the actions.lock) are + # exempt. + while IFS=: read -r line_no rest; do + [[ -z "$line_no" ]] && continue + [[ "$rest" =~ ^[[:space:]]*# ]] && continue + [[ "$rest" == *NOSONAR* ]] && continue + if [[ "$rest" =~ uses:[[:space:]]*[^[:space:]]+@v[0-9] ]]; then + emit_finding "medium" "ci_resilience" "uses_ref_not_sha" \ + "$wf" "$line_no" "workflow uses: ref is a version tag, not a full commit SHA (HYP-S011)" "CWE-1357" \ + "Resolve the tag to its commit SHA (the actions.lock carries it) and pin: uses: owner/action@<40-hex> # vX.Y.Z" + fi + done < <(grep -n 'uses:' "$wf" 2>/dev/null || true) done # ── WORKFLOW HYGIENE — detect irrelevant workflows per repo ──────── scan_workflow_hygiene "$dir" } +# ───────────────────────────────────────────────────────────────────── +# HYP-S010: CLADE identity coherence. +# A CLADE.a2ml must declare THIS repository and carry the recomputable +# UUIDv8 profile-C mint for that declaration. Catches the template- +# residue class (a repo declaring rsr-template-repo's identity verbatim) +# and the comment/uuid divergence class, both seen 2026-10-15. +# ───────────────────────────────────────────────────────────────────── +scan_clade_identity() { + local dir="$1" + local clade="$dir/.machine_readable/descriptiles/CLADE.a2ml" + [[ -f "$clade" ]] || return 0 + + local owner name uuid preimage + owner=$(grep -oP '^primary-owner = "\K[^"]+' "$clade" 2>/dev/null | head -1) + name=$(grep -oP '^canonical-name = "\K[^"]+' "$clade" 2>/dev/null | head -1) + uuid=$(grep -oP '^uuid = "\K[^"]+' "$clade" 2>/dev/null | head -1) + preimage=$(grep -oP 'gv-clade-index:github\.com/\K[a-zA-Z0-9._/-]+' "$clade" 2>/dev/null | head -1) + + # Template residue: the identity block declares the template. + if [[ "$name" == "rsr-template-repo" ]]; then + local origin_url="" + origin_url=$(git -C "$dir" remote get-url origin 2>/dev/null || true) + if [[ "$origin_url" != *rsr-template-repo* && "$dir" != *rsr-template-repo* ]]; then + emit_finding "high" "identity_integrity" "clade_template_residue" \ + "$clade" 0 "CLADE identity block declares rsr-template-repo but this repository is not the template (HYP-S010)" "CWE-1057" \ + "Declare THIS repository: primary-owner, canonical-name, prefixed-name, forges, born — then re-mint the uuid from that declaration (the fix direction is the owner's ruling, not the reviewer bot's)" + fi + fi + + # uuid must be recomputable from the declared owner/name (profile C). + if command -v python3 >/dev/null 2>&1 && [[ -n "$owner" && -n "$name" && -n "$uuid" ]]; then + local expected + expected=$(python3 -c ' +import hashlib, sys +pre = ("gv-clade-index:github.com/%s/%s" % (sys.argv[1].lower(), sys.argv[2])).encode() +b = bytearray(hashlib.sha256(pre).digest()[:16]) +b[6] = (b[6] & 0x0F) | 0x80 +b[8] = (b[8] & 0x3F) | 0x80 +h = b.hex() +print(f"{h[0:8]}-{h[8:12]}-{h[12:16]}-{h[16:20]}-{h[20:32]}")' "$owner" "$name" 2>/dev/null || true) + if [[ -n "$expected" && "$expected" != "$uuid" ]]; then + emit_finding "high" "identity_integrity" "clade_uuid_not_recomputable" \ + "$clade" 0 "CLADE uuid $uuid is not the profile-C mint for the declared $owner/$name (expected $expected) (HYP-S010)" "CWE-1057" \ + "Either the declaration or the uuid is stale: re-mint from the registered gv-clade-index domain over the preimage the declaration names, and sync CLAUDE.md" + fi + fi + + # The recorded preimage must identify the declared repository. + if [[ -n "$preimage" && -n "$owner" && -n "$name" ]]; then + local declared_lc repo_lc + declared_lc=$(printf '%s/%s' "$owner" "$name" | tr '[:upper:]' '[:lower:]') + repo_lc=$(printf '%s' "$preimage" | tr '[:upper:]' '[:lower:]') + if [[ "$declared_lc" != "$repo_lc" ]]; then + emit_finding "high" "identity_integrity" "clade_preimage_mismatch" \ + "$clade" 0 "CLADE preimage identifies $repo_lc but the identity block declares $declared_lc (HYP-S010)" "CWE-1057" \ + "Two true facts pointing at different repositories: the declaration is THIS repository's — re-mint the uuid from it, or correct the declaration by owner ruling" + fi + fi +} + # ───────────────────────────────────────────────────────────────────── # Workflow hygiene: detect workflows irrelevant to the repo's languages # and tech stack. Emits findings for each unnecessary workflow. @@ -924,6 +1001,7 @@ scan_directory() { scan_code_patterns "$dir" scan_repo_structure "$dir" scan_workflows "$dir" + scan_clade_identity "$dir" close_findings From 5150bd4893194af7eb4f774f26cfe175b81df038 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:56:41 +0000 Subject: [PATCH 2/3] docs(cli): document workflow, CLADE identity, and scan behavior --- hypatia-cli-bash.sh | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/hypatia-cli-bash.sh b/hypatia-cli-bash.sh index 545fdd64..050bc4b1 100755 --- a/hypatia-cli-bash.sh +++ b/hypatia-cli-bash.sh @@ -688,6 +688,12 @@ scan_repo_structure() { # ───────────────────────────────────────────────────────────────────── # Workflow-specific deep checks +# Scan $1/.github/workflows/*.yml and *.yaml for checkout with +# pull_request_target, version refs (HYP-S011), and workflow hygiene issues. +# HYP-S011 matches @v followed by a digit; comment-only lines and lines +# containing NOSONAR are exempt from that check. Other unpinned refs are +# not detected by HYP-S011. A missing workflows directory returns success. +# Append severity-filtered findings to the initialised findings accumulator. # ───────────────────────────────────────────────────────────────────── scan_workflows() { local dir="$1" @@ -727,10 +733,15 @@ scan_workflows() { # ───────────────────────────────────────────────────────────────────── # HYP-S010: CLADE identity coherence. -# A CLADE.a2ml must declare THIS repository and carry the recomputable -# UUIDv8 profile-C mint for that declaration. Catches the template- -# residue class (a repo declaring rsr-template-repo's identity verbatim) -# and the comment/uuid divergence class, both seen 2026-10-15. +# Check $1/.machine_readable/descriptiles/CLADE.a2ml for template residue, +# a UUIDv8 profile-C mismatch, and a recorded preimage that differs from +# the declared owner/name (compared without case). Template residue is +# exempt when the directory path or origin URL contains rsr-template-repo. +# UUID minting lowercases the owner but preserves the repository name; +# skip this check if python3 is unavailable or minting fails. +# Append severity-filtered findings to the initialised findings accumulator. +# A missing file returns success. Field extraction failures can terminate +# the scan under errexit/pipefail; origin lookup failures are tolerated. # ───────────────────────────────────────────────────────────────────── scan_clade_identity() { local dir="$1" @@ -973,6 +984,13 @@ scan_workflow_hygiene() { # ───────────────────────────────────────────────────────────────────── # Main scan orchestrator +# Scan repository directory $1 for code, structure, workflow and CLADE +# identity findings, filtered by HYPATIA_SEVERITY (default: medium). +# Write the report to stdout in HYPATIA_FORMAT: json includes metadata, +# github emits annotations, and any other value produces a text report. +# Reset the global findings accumulator; remove its temporary file after +# reporting. Return 0 for no reported findings, 1 otherwise. Uncaught +# command failures can terminate the scan before reporting or cleanup. # ───────────────────────────────────────────────────────────────────── scan_directory() { local dir="$1" From bccbc28fabe6d1b9669e8dd6aa0e2992ea59c283 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:57:57 +0000 Subject: [PATCH 3/3] fix(cli): avoid duplicate workflow action version-tag findings --- hypatia-cli-bash.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/hypatia-cli-bash.sh b/hypatia-cli-bash.sh index 050bc4b1..3bcf1cfc 100755 --- a/hypatia-cli-bash.sh +++ b/hypatia-cli-bash.sh @@ -136,11 +136,13 @@ run_pattern() { local sev="$4" type="$5" pattern="$6" cwe="$7" fix="$8" local auto_fixable="${9:-false}" local extra_rg_args="${10:-}" + local excluded_dir="${11:-}" local rg_cmd=(rg -n --no-heading --hidden --glob '!.git/' --glob "$glob" $extra_rg_args "$regex" "$dir") while IFS=: read -r file linenum rest; do [[ -z "$linenum" || ! "$linenum" =~ ^[0-9]+$ ]] && continue + [[ -n "$excluded_dir" && "$file" == "${excluded_dir%/}/"* ]] && continue # Truncate code to 200 chars to avoid jq issues local code="${rest:0:200}" emit_finding "$sev" "$type" "$pattern" "$file" "$linenum" "$code" "$cwe" "$fix" "$auto_fixable" @@ -508,8 +510,13 @@ scan_code_patterns() { # ── 9. WORKFLOW / CI SECURITY ────────────────────────────────── - # Unpinned GitHub Actions/reusable workflows (version tag or mutable branch) - run_pattern "$dir" "*.{yml,yaml}" 'uses:\s+[a-zA-Z0-9_.-]+/[a-zA-Z0-9_./-]+@(v[0-9][a-zA-Z0-9._-]*|main|master)' \ + # Version tags outside root workflows (handled by scan_workflows) + run_pattern "$dir" "*.{yml,yaml}" 'uses:\s+[a-zA-Z0-9_.-]+/[a-zA-Z0-9_./-]+@v[0-9][a-zA-Z0-9._-]*' \ + "high" "supply_chain" "unpinned_action" "CWE-829" \ + "Pin GitHub Actions/reusable workflows to full commit SHA" "true" "" "${dir%/}/.github/workflows" + + # Mutable branches in all YAML, including root workflows + run_pattern "$dir" "*.{yml,yaml}" 'uses:\s+[a-zA-Z0-9_.-]+/[a-zA-Z0-9_./-]+@(main|master)' \ "high" "supply_chain" "unpinned_action" "CWE-829" \ "Pin GitHub Actions/reusable workflows to full commit SHA" "true"