Skip to content
Merged
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
8 changes: 4 additions & 4 deletions bin/dry-run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@

unless ENV["BLOCKED_VERSIONS"].to_s.strip.empty?
# For example:
# [{"dependency-name":"event-stream","version":"= 3.3.6","reason":"malware"}]
# [{"dependency-name":"event-stream","version-requirement":"= 3.3.6","reason":"malware"}]
$options[:blocked_versions] = JSON.parse(ENV.fetch("BLOCKED_VERSIONS", nil))
end

Expand Down Expand Up @@ -730,9 +730,9 @@ def ignored_versions_for(dep)

def blocked_versions_for(dep)
$options[:blocked_versions]
.select { |bv| bv["dependency-name"] && bv["version"] }
.select { |bv| bv["dependency-name"] && bv["version-requirement"] }
.select { |bv| bv["dependency-name"].casecmp(dep.name).zero? }
.map { |bv| bv["version"] }
.map { |bv| bv["version-requirement"] }
end

def security_advisories
Expand Down Expand Up @@ -794,7 +794,7 @@ def security_fix?(dependency)
if $options[:blocked_versions].any?
puts "=> blocked versions active:"
$options[:blocked_versions].each do |bv|
msg = " #{bv['dependency-name']} #{bv['version']}"
msg = " #{bv['dependency-name']} #{bv['version-requirement']}"
msg += " (#{bv['reason']})" if bv["reason"]
puts msg
end
Expand Down
33 changes: 33 additions & 0 deletions updater/lib/dependabot/api_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,39 @@ def record_cooldown_meta(job)
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength

sig { params(package_manager: String).returns(T::Array[T::Hash[String, T.untyped]]) }
def fetch_blocked_versions(package_manager)
::Dependabot::OpenTelemetry.tracer.in_span("fetch_blocked_versions", kind: :internal) do |span|
span.set_attribute(::Dependabot::OpenTelemetry::Attributes::JOB_ID, job_id.to_s)

api_url = "#{http_client.params[:path]}/update_jobs/#{job_id}/blocked_versions"
response = http_client.get(path: api_url, query: { "package-manager": package_manager })

if response.status >= 400
Dependabot.logger.warn("Failed to fetch blocked versions (HTTP #{response.status}), continuing without them")
return []
end

parsed = JSON.parse(response.body)
Comment thread
kbukum1 marked this conversation as resolved.
unless parsed.is_a?(Hash)
Dependabot.logger.warn("Unexpected blocked versions format, continuing without them")
return []
end
data = parsed.fetch("data", [])
unless data.is_a?(Array) && data.all?(Hash)
Dependabot.logger.warn("Unexpected blocked versions format, continuing without them")
return []
end
Comment thread
kbukum1 marked this conversation as resolved.
data
rescue JSON::ParserError => e
Dependabot.logger.warn("Failed to parse blocked versions response: #{e.message}, continuing without them")
[]
rescue Excon::Error::Socket, Excon::Error::Timeout, OpenSSL::SSL::SSLError => e
Comment thread
kbukum1 marked this conversation as resolved.
Dependabot.logger.warn("Failed to fetch blocked versions: #{e.message}, continuing without them")
[]
end
end

private

# Update return type to allow returning a Hash or nil
Expand Down
14 changes: 7 additions & 7 deletions updater/lib/dependabot/job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -491,25 +491,25 @@ def log_ignore_conditions_for(dependency)
sig { params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
def blocked_versions_for(dependency)
matching_blocked_entries(dependency).filter_map do |bv|
version = bv["version"].strip
version.empty? ? nil : version
req = bv["version-requirement"].strip
Comment thread
kbukum1 marked this conversation as resolved.
req.empty? ? nil : req
end
end

sig { params(dependency: Dependabot::Dependency).void }
def log_blocked_versions_for(dependency)
entries = matching_blocked_entries(dependency).filter_map do |bv|
version = bv["version"].strip
next if version.empty?
req = bv["version-requirement"].strip
next if req.empty?

reason = bv["reason"].is_a?(String) ? bv["reason"].strip : nil
{ version: version, reason: reason&.empty? ? nil : reason }
{ version_requirement: req, reason: reason&.empty? ? nil : reason }
end
return if entries.empty?

Dependabot.logger.info("Blocked versions (by GitHub Security):")
entries.each do |entry|
msg = " #{entry[:version]}"
msg = " #{entry[:version_requirement]}"
msg += " - reason: #{entry[:reason]}" if entry[:reason]
Dependabot.logger.info(msg)
end
Expand All @@ -524,7 +524,7 @@ def matching_blocked_entries(dependency)
normalized_dep_name = T.must(normaliser).call(dependency.name)

blocked_versions
.select { |bv| bv["dependency-name"].is_a?(String) && bv["version"].is_a?(String) }
.select { |bv| bv["dependency-name"].is_a?(String) && bv["version-requirement"].is_a?(String) }
.select { |bv| T.must(normaliser).call(bv["dependency-name"]) == normalized_dep_name }
end

Expand Down
3 changes: 2 additions & 1 deletion updater/lib/dependabot/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ def initialize(client:)
:record_ecosystem_versions,
:increment_metric,
:record_ecosystem_meta,
:record_cooldown_meta
:record_cooldown_meta,
:fetch_blocked_versions

sig { void }
def wait_for_calls_to_finish
Expand Down
28 changes: 23 additions & 5 deletions updater/lib/dependabot/update_files_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,29 @@ def perform_job
sig { override.returns(Dependabot::Job) }
def job
@job ||= T.let(
Job.new_update_job(
job_id: job_id,
job_definition: Environment.job_definition,
repo_contents_path: Environment.repo_contents_path
),
begin
definition = JSON.parse(JSON.generate(Environment.job_definition))
job_hash = definition["job"]

# Fetch blocked versions from the API if the experiment is enabled.
# Check both the Experiments registry and the raw job definition since
# job-scoped experiments are not registered until Job construction.
# Inject them into the job definition so they're available at construction time.
experiments = job_hash["experiments"] || {}
if Experiments.enabled?(:dependabot_blocked_versions) ||
experiments["dependabot_blocked_versions"] ||
experiments["dependabot-blocked-versions"]
package_manager = job_hash["package-manager"] || job_hash["package_manager"] || ""
blocked = service.fetch_blocked_versions(package_manager)
job_hash["blocked-versions"] = blocked
end

Job.new_update_job(
job_id: job_id,
job_definition: definition,
repo_contents_path: Environment.repo_contents_path
)
end,
T.nilable(Dependabot::Job)
)
end
Expand Down
140 changes: 140 additions & 0 deletions updater/spec/dependabot/api_client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -800,4 +800,144 @@
end
end
end

describe "fetch_blocked_versions" do
let(:blocked_versions_url) { "http://example.com/update_jobs/1/blocked_versions" }

context "when the API returns blocked versions" do
before do
stub_request(:get, blocked_versions_url)
.with(query: { "package-manager": "npm_and_yarn" })
.to_return(
status: 200,
body: {
data: [
{ "dependency-name" => "event-stream", "version-requirement" => "= 3.3.6", "reason" => "malware" },
{ "dependency-name" => "flatmap-stream", "version-requirement" => "= 0.1.1", "reason" => "malware" }
]
}.to_json,
headers: headers
)
end

it "returns the blocked versions array" do
result = client.fetch_blocked_versions("npm_and_yarn")
expect(result).to eq(
[
{ "dependency-name" => "event-stream", "version-requirement" => "= 3.3.6", "reason" => "malware" },
{ "dependency-name" => "flatmap-stream", "version-requirement" => "= 0.1.1", "reason" => "malware" }
]
)
end
end

context "when the API returns an error" do
before do
stub_request(:get, blocked_versions_url)
.with(query: { "package-manager": "npm_and_yarn" })
.to_return(status: 500, body: "Internal Server Error", headers: headers)
end

it "returns an empty array and logs a warning" do
expect(Dependabot.logger).to receive(:warn).with(/Failed to fetch blocked versions/)
result = client.fetch_blocked_versions("npm_and_yarn")
expect(result).to eq([])
end
end

context "when the API times out" do
before do
stub_request(:get, blocked_versions_url)
.with(query: { "package-manager": "npm_and_yarn" })
.to_timeout
end

it "returns an empty array and logs a warning" do
expect(Dependabot.logger).to receive(:warn).with(/Failed to fetch blocked versions/)
result = client.fetch_blocked_versions("npm_and_yarn")
expect(result).to eq([])
end
end

context "when the API returns no blocked versions" do
before do
stub_request(:get, blocked_versions_url)
.with(query: { "package-manager": "npm_and_yarn" })
.to_return(
status: 200,
body: { data: [] }.to_json,
headers: headers
)
end

it "returns an empty array" do
result = client.fetch_blocked_versions("npm_and_yarn")
expect(result).to eq([])
end
end

context "when the API returns invalid JSON" do
before do
stub_request(:get, blocked_versions_url)
.with(query: { "package-manager": "npm_and_yarn" })
.to_return(status: 200, body: "not json", headers: headers)
end

it "returns an empty array and logs a warning" do
expect(Dependabot.logger).to receive(:warn).with(/Failed to parse blocked versions/)
result = client.fetch_blocked_versions("npm_and_yarn")
expect(result).to eq([])
end
end

context "when the API returns data that is not an array" do
before do
stub_request(:get, blocked_versions_url)
.with(query: { "package-manager": "npm_and_yarn" })
.to_return(
status: 200,
body: { data: "unexpected" }.to_json,
headers: headers
)
end

it "returns an empty array and logs a warning" do
expect(Dependabot.logger).to receive(:warn).with(/Unexpected blocked versions format/)
result = client.fetch_blocked_versions("npm_and_yarn")
expect(result).to eq([])
end
end

context "when the API returns a non-object JSON body" do
before do
stub_request(:get, blocked_versions_url)
.with(query: { "package-manager": "npm_and_yarn" })
.to_return(status: 200, body: "[]", headers: headers)
end

it "returns an empty array and logs a warning" do
expect(Dependabot.logger).to receive(:warn).with(/Unexpected blocked versions format/)
result = client.fetch_blocked_versions("npm_and_yarn")
expect(result).to eq([])
end
end

context "when the API returns data entries that are not hashes" do
before do
stub_request(:get, blocked_versions_url)
.with(query: { "package-manager": "npm_and_yarn" })
.to_return(
status: 200,
body: { data: [1, "not-a-hash"] }.to_json,
headers: headers
)
end

it "returns an empty array and logs a warning" do
expect(Dependabot.logger).to receive(:warn).with(/Unexpected blocked versions format/)
result = client.fetch_blocked_versions("npm_and_yarn")
expect(result).to eq([])
end
end
end
end
Loading
Loading