From 3418477d1c688ffd855eae8de553cce3a0295a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Wed, 5 Aug 2026 21:55:07 +0200 Subject: [PATCH 1/2] Allow a secret_scan package metadata key The repository scans published tarballs for leaked credentials. A package suppresses paths it deliberately ships secret-like values in, such as test fixtures, with: package: [secret_scan: [ignore: ["test/fixtures/**"]]] The key was dropped by the metadata whitelist; add it so the client writes it into hex_metadata.config for the repository to read. --- lib/hex/package.ex | 6 ++++++ lib/mix/tasks/hex.build.ex | 2 +- test/mix/tasks/hex.build_test.exs | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/hex/package.ex b/lib/hex/package.ex index 6565458e..67b1e5e8 100644 --- a/lib/hex/package.ex +++ b/lib/hex/package.ex @@ -53,6 +53,12 @@ defmodule Hex.Package do package. If a `rebar` or `rebar.config` file is present Hex will mark it as able to build with rebar. This detection can be overridden by setting this field. + * `:secret_scan` - Configuration for the repository's secret scanning of + the package. Supports `:ignore`, a list of file path globs whose + findings are suppressed, for paths that deliberately ship secret-like + values such as test fixtures: + + secret_scan: [ignore: ["test/fixtures/**"]] """ end end diff --git a/lib/mix/tasks/hex.build.ex b/lib/mix/tasks/hex.build.ex index 453e93d8..b7b4ed86 100644 --- a/lib/mix/tasks/hex.build.ex +++ b/lib/mix/tasks/hex.build.ex @@ -33,7 +33,7 @@ defmodule Mix.Tasks.Hex.Build do @error_fields ~w(app name files version build_tools)a @warn_fields ~w(description licenses links)a - @meta_fields @error_fields ++ @warn_fields ++ ~w(elixir extra)a + @meta_fields @error_fields ++ @warn_fields ++ ~w(elixir extra secret_scan)a @root_fields ~w(app version elixir description)a @max_description_length 300 @default_repo "hexpm" diff --git a/test/mix/tasks/hex.build_test.exs b/test/mix/tasks/hex.build_test.exs index 2411263c..3def0c25 100644 --- a/test/mix/tasks/hex.build_test.exs +++ b/test/mix/tasks/hex.build_test.exs @@ -403,6 +403,25 @@ defmodule Mix.Tasks.Hex.BuildTest do purge([ReleaseMeta.MixProject]) end + test "keeps secret_scan config in the metadata" do + meta = + Mix.Tasks.Hex.Build.package( + %{ + secret_scan: [ignore: ["test/fixtures/**"]], + files: [], + app: :demo, + name: "demo", + description: "d", + licenses: ["MIT"], + build_tools: ["mix"] + }, + app: :demo, + version: "1.0.0" + ) + + assert meta[:secret_scan] == [ignore: ["test/fixtures/**"]] + end + test "reject package if description is missing" do Process.put(:hex_test_app_name, :build_no_description) Mix.Project.push(ReleaseNoDescription.MixProject) From 4f06e2e43ce0b67d418c583bdc82092daf017e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Thu, 6 Aug 2026 18:40:35 +0200 Subject: [PATCH 2/2] Print keyword list metadata instead of crashing mix hex.build raised Protocol.UndefinedError for a package setting secret_scan: format_metadata_value/1 joined the list and its elements are tuples. Render a keyword list the way a map is already rendered. :extra given a keyword list hit the same path. The previous test only called package/2, so it never reached the print path. It is now a build that goes through the task and asserts both the printed line and the metadata in hex_metadata.config. --- lib/mix/tasks/hex.build.ex | 10 ++++++++- test/mix/tasks/hex.build_test.exs | 37 +++++++++++++++++-------------- test/support/release_samples.ex | 16 +++++++++++++ 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/lib/mix/tasks/hex.build.ex b/lib/mix/tasks/hex.build.ex index b7b4ed86..d1fd3fe1 100644 --- a/lib/mix/tasks/hex.build.ex +++ b/lib/mix/tasks/hex.build.ex @@ -367,8 +367,16 @@ defmodule Mix.Tasks.Hex.Build do end end + defp format_metadata_value([]) do + "" + end + defp format_metadata_value(list) when is_list(list) do - Enum.join(list, ", ") + if Keyword.keyword?(list) do + format_metadata_value(Map.new(list)) + else + Enum.join(list, ", ") + end end defp format_metadata_value(map) when is_map(map) do diff --git a/test/mix/tasks/hex.build_test.exs b/test/mix/tasks/hex.build_test.exs index 3def0c25..b4ba52c7 100644 --- a/test/mix/tasks/hex.build_test.exs +++ b/test/mix/tasks/hex.build_test.exs @@ -403,23 +403,26 @@ defmodule Mix.Tasks.Hex.BuildTest do purge([ReleaseMeta.MixProject]) end - test "keeps secret_scan config in the metadata" do - meta = - Mix.Tasks.Hex.Build.package( - %{ - secret_scan: [ignore: ["test/fixtures/**"]], - files: [], - app: :demo, - name: "demo", - description: "d", - licenses: ["MIT"], - build_tools: ["mix"] - }, - app: :demo, - version: "1.0.0" - ) - - assert meta[:secret_scan] == [ignore: ["test/fixtures/**"]] + test "create with secret_scan" do + Process.put(:hex_test_app_name, :build_secret_scan) + Mix.Project.push(ReleaseSecretScan.MixProject) + + in_tmp(fn -> + Hex.State.put(:cache_home, tmp_path()) + + File.write!("myfile.txt", "hello") + File.chmod!("myfile.txt", 0o100644) + + Mix.Tasks.Hex.Build.run(["--unpack"]) + + # A keyword list has to print like a map does, not blow up on the tuples. + assert_received {:mix_shell, :info, [" Secret scan: \n ignore: test/fixtures/**"]} + + {:ok, metadata} = :file.consult("build_secret_scan-0.0.1/hex_metadata.config") + assert {"secret_scan", [{"ignore", ["test/fixtures/**"]}]} in metadata + end) + after + purge([ReleaseSecretScan.MixProject]) end test "reject package if description is missing" do diff --git a/test/support/release_samples.ex b/test/support/release_samples.ex index 880554a4..42d2a6e9 100644 --- a/test/support/release_samples.ex +++ b/test/support/release_samples.ex @@ -29,6 +29,22 @@ defmodule ReleaseSimple.MixProject do end end +defmodule ReleaseSecretScan.MixProject do + def project do + [ + app: Process.get(:hex_test_app_name) || raise("missing app name"), + description: "baz", + version: "0.0.1", + package: [ + licenses: ["MIT"], + files: ["myfile.txt"], + links: %{"a" => "http://a"}, + secret_scan: [ignore: ["test/fixtures/**"]] + ] + ] + end +end + defmodule ReleaseNewSimple.MixProject do def project do [