From b383356c41a4f946920f2dca7ac178c5ef398dbf Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:11:34 +0100 Subject: [PATCH 1/2] fix(cli): JSON output reports warn as medium for pre-bd9313a6 callers (D260) Since a63c432 (#763) the ResearchExtensions rules emit severity "warn". Callers pinned to standards' hypatia-scan-reusable.yml before bd9313a6 (8f2ee508, 81dbf2dd, 571cc734, 84355587, 092deda) validate the JSON findings against critical/high/medium/low/info and reject the whole array on a single "warn", failing with "Hypatia did not produce one valid findings array" on ~350 repos. The reusable clones hypatia HEAD, so mapping warn -> medium in the JSON sink clears every such caller at once. warn already ranks with medium in @severity_order, so no information is lost. SARIF ("warning") and GitHub output are unchanged. Removal criterion (owner ruling D260, standards#787): drop the shim once an enumerated census of hypatia-scan callers shows none pinned to a rejecting copy of the reusable. Test: CLI JSON output on the RE tripwire repo contains "medium", never "warn", and only the five accepted severities. Mutant (shim passes warn through) turns it red: 11 tests, 1 failure. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YJ6PbZUYBcjJv7FTRfyogo --- lib/hypatia/cli.ex | 11 +++++++- test/research_extensions_wiring_test.exs | 34 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/hypatia/cli.ex b/lib/hypatia/cli.ex index 112cb000..1179ebb8 100644 --- a/lib/hypatia/cli.ex +++ b/lib/hypatia/cli.ex @@ -1199,9 +1199,18 @@ defmodule Hypatia.CLI do # ─── Output formatting ─────────────────────────────────────────────── defp output(findings, "json") do - IO.puts(Jason.encode!(findings, pretty: true)) + IO.puts(Jason.encode!(Enum.map(findings, &json_compat_severity/1), pretty: true)) end + # Compatibility shim (standards#787 D260). Callers pinned to standards' + # hypatia-scan-reusable.yml before bd9313a6 validate the JSON against + # critical/high/medium/low/info and reject the whole array on one "warn", + # so ~350 repos failed with "Hypatia did not produce one valid findings + # array". warn already ranks with medium (@severity_order), so this loses + # no information. Remove once no caller is pinned to a rejecting copy. + defp json_compat_severity(%{severity: "warn"} = f), do: %{f | severity: "medium"} + defp json_compat_severity(f), do: f + defp output(findings, "sarif") do # SARIF 2.1.0 — see lib/hypatia/sarif.ex. Output goes to stdout # so workflows can redirect to a .sarif file and upload-sarif diff --git a/test/research_extensions_wiring_test.exs b/test/research_extensions_wiring_test.exs index b232befd..69cbe228 100644 --- a/test/research_extensions_wiring_test.exs +++ b/test/research_extensions_wiring_test.exs @@ -161,6 +161,40 @@ defmodule Hypatia.Rules.ResearchExtensionsWiringTest do assert summary =~ ~r/warn=[1-9][0-9]*/ end + # Callers on standards' hypatia-scan-reusable.yml before bd9313a6 reject + # the whole JSON array if any finding carries severity "warn" (D260). + test "CLI JSON output reports warn findings as medium, never as warn" do + repo = tripwire_repo() + + json = + ExUnit.CaptureIO.capture_io(fn -> + ExUnit.CaptureIO.capture_io(:stderr, fn -> + CLI.main([ + "scan", + repo, + "--rules", + "research_extensions", + "--format", + "json", + "--exit-zero" + ]) + end) + end) + + findings = Jason.decode!(json) + severities = Enum.map(findings, & &1["severity"]) + + assert "medium" in severities, + "the tripwire repo emits warn-tier RE findings; none reached JSON" + + refute "warn" in severities, + "a single \"warn\" makes every pre-bd9313a6 standards validator " <> + "reject the whole findings array" + + allowed = ["critical", "high", "medium", "low", "info"] + assert Enum.all?(severities, &(&1 in allowed)) + end + # Six of the ten RE rules emit `severity: :warn`. "warn" was absent from # CLI's @severity_order, so `Map.get(@severity_order, "warn", 5)` gave it # rank 5; the filter `rank <= threshold` at the default threshold of From eccf07c62686f44f99a4cc224fdde04d19bdcc87 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:17:41 +0100 Subject: [PATCH 2/2] fix(cli): group output/2 clauses so --warnings-as-errors compiles The D260 shim helpers sat between the output/2 clauses, which Elixir warns about ("clauses with the same name and arity should be grouped together"). escript-soundness.yml compiles with --warnings-as-errors, so the job went red. Move json_compat_severity/1 below the last output/2 clause; behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YJ6PbZUYBcjJv7FTRfyogo --- lib/hypatia/cli.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/hypatia/cli.ex b/lib/hypatia/cli.ex index 1179ebb8..2d76aa86 100644 --- a/lib/hypatia/cli.ex +++ b/lib/hypatia/cli.ex @@ -1202,15 +1202,6 @@ defmodule Hypatia.CLI do IO.puts(Jason.encode!(Enum.map(findings, &json_compat_severity/1), pretty: true)) end - # Compatibility shim (standards#787 D260). Callers pinned to standards' - # hypatia-scan-reusable.yml before bd9313a6 validate the JSON against - # critical/high/medium/low/info and reject the whole array on one "warn", - # so ~350 repos failed with "Hypatia did not produce one valid findings - # array". warn already ranks with medium (@severity_order), so this loses - # no information. Remove once no caller is pinned to a rejecting copy. - defp json_compat_severity(%{severity: "warn"} = f), do: %{f | severity: "medium"} - defp json_compat_severity(f), do: f - defp output(findings, "sarif") do # SARIF 2.1.0 — see lib/hypatia/sarif.ex. Output goes to stdout # so workflows can redirect to a .sarif file and upload-sarif @@ -1257,6 +1248,15 @@ defmodule Hypatia.CLI do end end + # Compatibility shim (standards#787 D260). Callers pinned to standards' + # hypatia-scan-reusable.yml before bd9313a6 validate the JSON against + # critical/high/medium/low/info and reject the whole array on one "warn", + # so ~350 repos failed with "Hypatia did not produce one valid findings + # array". warn already ranks with medium (@severity_order), so this loses + # no information. Remove once no caller is pinned to a rejecting copy. + defp json_compat_severity(%{severity: "warn"} = f), do: %{f | severity: "medium"} + defp json_compat_severity(f), do: f + defp output_report(findings, repo_path) do IO.puts("=" |> String.duplicate(72)) IO.puts(" Hypatia Security & Policy Report")