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
77 changes: 71 additions & 6 deletions docker/lib/dependabot/docker/metadata_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,89 @@ class MetadataFinder < Dependabot::MetadataFinders::Base

private

# Finds the repository for the Docker image using OCI annotations.
# @see https://specs.opencontainers.org/image-spec/annotations/
sig { override.returns(T.nilable(Dependabot::Source)) }
def look_up_source
return if dependency.requirements.empty?

new_source = dependency.requirements.first&.fetch(:source)
return unless new_source && new_source[:registry] && new_source[:tag]
return unless new_source && new_source[:registry] && (new_source[:tag] || new_source[:digest])

image_ref = "#{new_source[:registry]}/#{dependency.name}:#{new_source[:tag]}"
image_details_output = SharedHelpers.run_shell_command("regctl image inspect #{image_ref}")
image_details = JSON.parse(image_details_output)
image_source = image_details.dig("config", "Labels", "org.opencontainers.image.source")
details = image_details(new_source)
image_source = details.dig("config", "Labels", "org.opencontainers.image.source")
# Return early if the org.opencontainers.image.source label is not present
return unless image_source

Dependabot::Source.from_url(image_source)
# If we have a tag, return the source directly without additional version metadata
return Dependabot::Source.from_url(image_source) if new_source[:tag]

# If we only have a digest, we need to look for the version label to build the source
build_source_from_image_version(image_source, details)
rescue StandardError => e
Dependabot.logger.warn("Error looking up Docker source: #{e.message}")
nil
end

sig do
params(
source: T::Hash[Symbol, T.untyped]
).returns(
T::Hash[String, T.untyped]
)
end
def image_details(source)
registry = source[:registry]
tag = source[:tag]
digest = source[:digest]

image_ref =
# If both tag and digest are present, use the digest as docker ignores the tag when a digest is present
if digest
Comment thread
kbukum1 marked this conversation as resolved.
"#{registry}/#{dependency.name}@sha256:#{digest}"
else
"#{registry}/#{dependency.name}:#{tag}"
end

Dependabot.logger.info("Looking up Docker source #{image_ref}")
output = SharedHelpers.run_shell_command("regctl image inspect #{image_ref}")
JSON.parse(output)
end

# Builds a Dependabot::Source object using the OCI image version label.
#
# This is used as a fallback when an image is referenced by digest rather than a tag
sig do
params(
image_source: String,
details: T::Hash[String, T.untyped]
).returns(T.nilable(Dependabot::Source))
end
def build_source_from_image_version(image_source, details)
image_version = details.dig("config", "Labels", "org.opencontainers.image.version")
revision = details.dig("config", "Labels", "org.opencontainers.image.revision")
# Sometimes the versions are not tags (e.g., "24.04")
# We only want to build a source if the version looks like a tag (starts with "v")
# This is a safeguard for a first iteration. We may adjust this later based on user feedback.
tag_like = image_version&.start_with?("v")
Comment thread
kbukum1 marked this conversation as resolved.

return unless tag_like || revision
Comment thread
kbukum1 marked this conversation as resolved.

parsed_source = Dependabot::Source.from_url(image_source)
return unless parsed_source

branch_info = image_version ? "image version '#{image_version}'" : "unknown image version"
commit_info = revision ? "revision '#{revision}'" : "no commit"
Dependabot.logger.info "Building source with #{branch_info} and #{commit_info}"

Dependabot::Source.new(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if there is any easier way to do this other than to change how Source.from_url works which may cause undesired side-effects. I am open for feedback/ideas 👀

provider: parsed_source.provider,
repo: parsed_source.repo,
directory: parsed_source.directory,
branch: image_version,
commit: revision
)
end
end
end
end
Expand Down
99 changes: 98 additions & 1 deletion docker/spec/dependabot/docker/metadata_finder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,53 @@
end
end

context "with a docker image with both tag and sha that has an OCI source annotation" do
let(:dependency_with_tag_and_sha_source) do
Dependabot::Dependency.new(
name: "dependabot-fixtures/docker-with-source",
version: "v0.0.2",
requirements: [{
file: "Dockerfile",
requirement: nil,
groups: [],
source: { registry: "ghcr.io",
digest: "389a5a9a5457ed237b05d623ddc31a42fa97811051dcd02d7ca4ad46bd3edd3e",
tag: "v0.0.2" }
}],
package_manager: "docker"
)
end

let(:dependency) { dependency_with_tag_and_sha_source }

it "finds the repository" do
expect(finder.source_url).to eq "https://github.com/dependabot-fixtures/docker-with-source"
end
end

context "with a digest but no tag or revision data" do
let(:dependency_with_sha_no_tag) do
Dependabot::Dependency.new(
name: "dependabot/dependabot-updater-npm",
version: "",
requirements: [{
file: "Dockerfile",
requirement: nil,
groups: [],
source: { registry: "ghcr.io",
digest: "74c21f5886502d754c47a163975062e0d3065e3d19f43c8f48c9dbeb2126767e" }
}],
package_manager: "docker"
)
end

let(:dependency) { dependency_with_sha_no_tag }

it "does not find the repository" do
expect(finder.source_url).to be_nil
end
end

context "with a docker image that lacks an OCI source annotation" do
let(:dependency) { dependency_without_source }

Expand All @@ -73,7 +120,57 @@
requirement: nil,
groups: [],
source: { registry: "ghcr.io",
digest: "sha256:389a5a9a5457ed237b05d623ddc31a42fa97811051dcd02d7ca4ad46bd3edd3e" }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only change I applied to this test is to remove the sha256 prefix as the spec was misleading. Our source parser drops sha256 currently

digest: "389a5a9a5457ed237b05d623ddc31a42fa97811051dcd02d7ca4ad46bd3edd3e" }
Comment thread
kbukum1 marked this conversation as resolved.
}],
package_manager: "docker"
)
end

it "doesn't find the repository" do
expect(finder.source_url).to be_nil
end
end

context "with a docker image without a tag but with org.opencontainers.image.version populated" do
let(:dependency) do
Dependabot::Dependency.new(
name: "regclient/regctl",
version: "",
requirements: [{
file: "Dockerfile",
requirement: nil,
groups: [],
source: { registry: "ghcr.io",
digest: "a734f285c0962e46557bff24489fa0b0521455733f72d9eb30c4f7a5027aeed6" }
}],
package_manager: "docker"
)
end

it "finds the repository" do
expect(finder.source_url).to eq "https://github.com/regclient/regclient"
# Normally, accessing private methods in tests is discouraged.
# In this case, we need to verify the branch and commit derived from the image within the source
# to ensure the source construction logic is correct. This access is for internal validation only.
# Exposing the source publicly only for this test would be less desirable.
expect(finder.send(:source).branch).to eq "v0.11.1"
expect(finder.send(:source).commit).to eq "bf3bcfc47173b49ee8000d1d3a1ac15036e83cf0"
Comment thread
kbukum1 marked this conversation as resolved.
end
Comment thread
kbukum1 marked this conversation as resolved.
end

context "with a docker image without a tag but without a proper tag format or revision" do
# The image used here has org.opencontainers.image.version set to "24.04"
# which refers to the Ubuntu version rather than a tag
let(:dependency) do
Dependabot::Dependency.new(
name: "maven",
version: "",
requirements: [{
file: "Dockerfile",
requirement: nil,
groups: [],
source: { registry: "docker.io",
digest: "800a33a4cb190082c47abcd57944c852e1dece834f92c0aef65bea6336c52a72" }
}],
package_manager: "docker"
)
Expand Down
Loading