From a2eaf602b4ea591e291f7556bea36424456fa0c6 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 18 May 2026 22:12:36 +0100 Subject: [PATCH] =?UTF-8?q?fix(governance)!:=20banned=5Flanguage=20ban=20i?= =?UTF-8?q?s=20total=20=E2=80=94=20no=20exceptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Org policy 2026-05-18: the banned-language ban (incl. Python) is now absolute, with no SaltStack-style carve-outs. - ScannerSuppression.suppressed?/4 gains a hard short-circuit clause: cicd_rules/banned_language_file is NEVER suppressed — not by .hypatia-ignore, built-in default exemptions, universal excludes, or training-corpus paths. (Added the canonical bodiless default head so the guard clause coexists with the optional opts arg.) - cicd_rules :python_detected loses its `exception: "SaltStack"` field. - 4 new tests pin the invariant (unsuppressible via every vector; unrelated rules on the same path remain suppressible). Compiles clean; 24/24 scanner_suppression tests pass. Companion to #279 (which removed hypatia's own Python + revoked its exemption records). Note: the #272 `unwrap_without_check` false positive is NOT a rule defect — the source regex /\.unwrap\(\)/ does not match `unwrap_or` (verified); it was a stale compiled escript, already addressed by #278's escript rebuild. No rule change there. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/hypatia/scanner_suppression.ex | 15 +++++++++- lib/rules/cicd_rules.ex | 6 ++-- test/scanner_suppression_test.exs | 45 ++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/lib/hypatia/scanner_suppression.ex b/lib/hypatia/scanner_suppression.ex index d84a819c..0fdc7340 100644 --- a/lib/hypatia/scanner_suppression.ex +++ b/lib/hypatia/scanner_suppression.ex @@ -78,8 +78,21 @@ defmodule Hypatia.ScannerSuppression do `file` is matched as a normalised relative path. Absolute paths are normalised by stripping the leading repo_path when supplied. + + ## Unsuppressible findings + + The banned-language ban (`cicd_rules/banned_language_file`) is **total + with no exceptions** as of org policy 2026-05-18 — including the former + SaltStack carve-out. No `.hypatia-ignore` entry, built-in default + exemption, universal exclude, or training-corpus path may suppress it; + this clause short-circuits every suppression vector for that rule so the + gate cannot be silenced repo-side. """ - def suppressed?(file, rule_module, rule_type, opts \\ []) do + def suppressed?(file, rule_module, rule_type, opts \\ []) + + def suppressed?(_file, "cicd_rules", "banned_language_file", _opts), do: false + + def suppressed?(file, rule_module, rule_type, opts) do repo_path = Keyword.get(opts, :repo_path, nil) rel = relative(file, repo_path) diff --git a/lib/rules/cicd_rules.ex b/lib/rules/cicd_rules.ex index c1f1d4b9..3685490b 100644 --- a/lib/rules/cicd_rules.ex +++ b/lib/rules/cicd_rules.ex @@ -65,8 +65,10 @@ defmodule Hypatia.Rules.CicdRules do %{id: :typescript_detected, glob: "*.ts", reason: "TypeScript banned -- use ReScript"}, %{id: :nodejs_detected, glob: "package-lock.json", reason: "Node.js banned -- use Deno"}, %{id: :golang_detected, glob: "*.go", reason: "Go banned -- use Rust"}, - %{id: :python_detected, glob: "*.py", reason: "Python banned -- use Julia/Rust", - exception: "SaltStack"}, + # Python ban is total — no exceptions (the former SaltStack carve-out + # was removed by org policy 2026-05-18). ScannerSuppression also + # hard-refuses to suppress cicd_rules/banned_language_file. + %{id: :python_detected, glob: "*.py", reason: "Python banned -- use Julia/Rust"}, %{id: :makefile_detected, glob: "Makefile", reason: "Makefiles banned -- use justfile"}, %{id: :unpinned_action, pattern: ~r/uses:\s+[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.\/-]+@(v[0-9][a-zA-Z0-9.-]*|main|master)/, diff --git a/test/scanner_suppression_test.exs b/test/scanner_suppression_test.exs index 1433167b..ffca6e55 100644 --- a/test/scanner_suppression_test.exs +++ b/test/scanner_suppression_test.exs @@ -68,6 +68,51 @@ defmodule Hypatia.ScannerSuppressionTest do end end + describe "suppressed?/4 — banned_language_file is total, no exceptions" do + test "never suppressed even on a universal-exclude path" do + refute ScannerSuppression.suppressed?( + "node_modules/tool/helper.py", + "cicd_rules", + "banned_language_file" + ) + end + + test "never suppressed even for a training-corpus path" do + refute ScannerSuppression.suppressed?( + ".audittraining/security-errors/sample.py", + "cicd_rules", + "banned_language_file" + ) + end + + test "never suppressed even with a matching .hypatia-ignore entry" do + tmp = Path.join(System.tmp_dir!(), "hyp-ban-#{System.unique_integer([:positive])}") + File.mkdir_p!(Path.join(tmp, "scripts")) + + File.write!( + Path.join(tmp, ".hypatia-ignore"), + "cicd_rules/banned_language_file:scripts/legacy.py\n" + ) + + refute ScannerSuppression.suppressed?( + "scripts/legacy.py", + "cicd_rules", + "banned_language_file", + repo_path: tmp + ) + + File.rm_rf!(tmp) + end + + test "an unrelated rule on the same path is still suppressible" do + assert ScannerSuppression.suppressed?( + "node_modules/foo/index.js", + "security_errors", + "secret_detected" + ) + end + end + describe "context_safe_line?/2 — line-level exemptions for secret_detected" do test "GitHub Actions secrets reference is not a leak" do assert ScannerSuppression.context_safe_line?(