diff --git a/lib/hypatia/cli.ex b/lib/hypatia/cli.ex index e3e4a7fc..83b10472 100644 --- a/lib/hypatia/cli.ex +++ b/lib/hypatia/cli.ex @@ -1202,7 +1202,7 @@ 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 defp output(findings, "sarif") do @@ -1251,6 +1251,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") 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