From 6c02db353129e6a8bcd3b53c5b24d3c88075edd9 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Sat, 4 Apr 2026 12:51:37 +0100 Subject: [PATCH] Add npm and pip cooldown defaults - delay new `npm` and `pip` packages by one day so freshly published compromises are less likely to land in builds - apply the `npm` cooldown in shared helpers so Node formula dependency installs inherit it too - cover the new defaults in `node_spec.rb` and `formula_spec.rb` --- Library/Homebrew/formula.rb | 4 ++++ Library/Homebrew/language/node.rb | 6 +++++ Library/Homebrew/test/formula_spec.rb | 14 ++++++++++++ Library/Homebrew/test/language/node_spec.rb | 25 ++++++++++++++++----- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 3b95f32417707..d6ee79024f3d9 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -44,6 +44,7 @@ require "api_hashable" require "utils/output" require "pypi_packages" +require "time" # A formula provides instructions and metadata for Homebrew to install a piece # of software. Every Homebrew formula is a {Formula}. @@ -2132,6 +2133,9 @@ def std_npm_args(prefix: libexec, ignore_scripts: true) } def std_pip_args(prefix: self.prefix, build_isolation: false) args = ["--verbose", "--no-deps", "--no-binary=:all:", "--ignore-installed", "--no-compile"] + # Delay packages published in the last day so builds are less likely to + # install a freshly compromised PyPI release. + args << "--uploaded-prior-to=#{(time - (24 * 60 * 60)).iso8601(0)}" args << "--prefix=#{prefix}" if prefix args << "--no-build-isolation" unless build_isolation args diff --git a/Library/Homebrew/language/node.rb b/Library/Homebrew/language/node.rb index 4a37176229bbb..531ae3542a260 100644 --- a/Library/Homebrew/language/node.rb +++ b/Library/Homebrew/language/node.rb @@ -66,10 +66,13 @@ def self.std_npm_install_args(libexec, ignore_scripts: true) (libexec/"lib").mkpath # npm install args for global style module format installed into libexec + # Delay packages published in the last day so builds are less likely to + # install a freshly compromised npm release or dependency. args = %W[ --loglevel=silly --global --build-from-source + --min-release-age=1 --#{npm_cache_config} --prefix=#{libexec} #{Dir.pwd}/#{pack} @@ -85,9 +88,12 @@ def self.std_npm_install_args(libexec, ignore_scripts: true) def self.local_npm_install_args(ignore_scripts: true) setup_npm_environment # npm install args for local style module format + # Delay packages published in the last day so builds are less likely to + # install a freshly compromised npm release or dependency. args = %W[ --loglevel=silly --build-from-source + --min-release-age=1 --#{npm_cache_config} ] diff --git a/Library/Homebrew/test/formula_spec.rb b/Library/Homebrew/test/formula_spec.rb index 87636f39d0baf..076af4fb82a64 100644 --- a/Library/Homebrew/test/formula_spec.rb +++ b/Library/Homebrew/test/formula_spec.rb @@ -2502,5 +2502,19 @@ def install expect(described_class.all(eval_all: true)).to eq([]) end end + + describe "#std_pip_args" do + let(:f) do + formula do + url "foo-1.0" + end + end + + it "filters packages uploaded within the last day" do + allow(f).to receive(:time).and_return(Time.utc(2026, 4, 4, 12, 0, 0)) + + expect(f.std_pip_args).to include("--uploaded-prior-to=2026-04-03T12:00:00Z") + end + end end # rubocop:enable Lint/DuplicateMethods diff --git a/Library/Homebrew/test/language/node_spec.rb b/Library/Homebrew/test/language/node_spec.rb index 2ed36ac298e56..5eedd0869996b 100644 --- a/Library/Homebrew/test/language/node_spec.rb +++ b/Library/Homebrew/test/language/node_spec.rb @@ -7,6 +7,10 @@ let(:npm_pack_cmd) { ["npm", "pack", "--ignore-scripts"] } describe "#setup_npm_environment" do + before do + described_class.instance_variable_set(:@env_set, false) + end + it "calls prepend_path when node formula exists only during the first call" do node = formula "node" do url "node-test-v1.0" @@ -15,7 +19,6 @@ without_partial_double_verification do expect(ENV).to receive(:prepend_path) end - described_class.instance_variable_set(:@env_set, false) described_class.setup_npm_environment expect(described_class.instance_variable_get(:@env_set)).to be(true) @@ -26,6 +29,7 @@ end it "does not call prepend_path when node formula does not exist" do + allow(Formula).to receive(:[]).with("node").and_raise(FormulaUnavailableError.new("node")) without_partial_double_verification do expect(ENV).not_to receive(:prepend_path) end @@ -50,6 +54,10 @@ describe "#std_npm_install_args" do let(:npm_install_arg) { Pathname("libexec") } + before do + allow(described_class).to receive(:setup_npm_environment) + end + it "raises error with non zero exitstatus" do allow(Utils).to receive(:popen_read).with(*npm_pack_cmd).and_return(`false`) expect { described_class.std_npm_install_args(npm_install_arg) }.to raise_error("npm failed to pack #{Dir.pwd}") @@ -63,12 +71,19 @@ it "does not raise error with a zero exitstatus" do allow(Utils).to receive(:popen_read).with(*npm_pack_cmd).and_return(`echo pack.tgz`) resp = described_class.std_npm_install_args(npm_install_arg) - expect(resp).to include("--prefix=#{npm_install_arg}", "#{Dir.pwd}/pack.tgz") + expect(resp).to include("--min-release-age=1", "--prefix=#{npm_install_arg}", "#{Dir.pwd}/pack.tgz") end end - specify "#local_npm_install_args" do - resp = described_class.local_npm_install_args - expect(resp).to include("--loglevel=silly", "--build-from-source", "--cache=#{HOMEBREW_CACHE}/npm_cache") + describe "#local_npm_install_args" do + before do + allow(described_class).to receive(:setup_npm_environment) + end + + it "includes the default npm install arguments" do + resp = described_class.local_npm_install_args + expect(resp).to include("--loglevel=silly", "--build-from-source", "--cache=#{HOMEBREW_CACHE}/npm_cache", + "--min-release-age=1") + end end end