Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion lib/hypatia/scanner_suppression.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 4 additions & 2 deletions lib/rules/cicd_rules.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)/,
Expand Down
45 changes: 45 additions & 0 deletions test/scanner_suppression_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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?(
Expand Down