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?(