Skip to content

Commit 176fc03

Browse files
committed
docker: add metadata finding support for org.opencontainers.image.version and org.opencontainers.image.revision
Extends metadata extraction to handle digest-only image references, where no tag is present. This new logic will run when one of the following additional OCI metadata fields are present along with the digest: - `org.opencontainers.image.version`: Packaged software version. This can sometimes be a tag - `org.opencontainers.image.revision`: Source control revision identifier for the packaged software
1 parent 039a54c commit 176fc03

File tree

2 files changed

+169
-7
lines changed

2 files changed

+169
-7
lines changed

docker/lib/dependabot/docker/metadata_finder.rb

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,89 @@ class MetadataFinder < Dependabot::MetadataFinders::Base
1313

1414
private
1515

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

2022
new_source = dependency.requirements.first&.fetch(:source)
21-
return unless new_source && new_source[:registry] && new_source[:tag]
23+
return unless new_source && new_source[:registry] && (new_source[:tag] || new_source[:digest])
2224

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

29-
Dependabot::Source.from_url(image_source)
30+
# If we have a tag, return the source directly without additional version metadata
31+
return Dependabot::Source.from_url(image_source) if new_source[:tag]
32+
33+
# If we only have a digest, we need to look for the version label to build the source
34+
build_source_from_image_version(image_source, details)
3035
rescue StandardError => e
3136
Dependabot.logger.warn("Error looking up Docker source: #{e.message}")
3237
nil
3338
end
39+
40+
sig do
41+
params(
42+
source: T::Hash[Symbol, T.untyped]
43+
).returns(
44+
T::Hash[String, T.untyped]
45+
)
46+
end
47+
def image_details(source)
48+
registry = source[:registry]
49+
tag = source[:tag]
50+
digest = source[:digest]
51+
52+
image_ref =
53+
# If both tag and digest are present, use the digest as docker ignores the tag when a digest is present
54+
if digest
55+
"#{registry}/#{dependency.name}@sha256:#{digest}"
56+
else
57+
"#{registry}/#{dependency.name}:#{tag}"
58+
end
59+
60+
Dependabot.logger.info("Looking up Docker source #{image_ref}")
61+
output = SharedHelpers.run_shell_command("regctl image inspect #{image_ref}")
62+
JSON.parse(output)
63+
end
64+
65+
# Builds a Dependabot::Source object using the OCI image version label.
66+
#
67+
# This is used as a fallback when an image is referenced by digest rather than a tag
68+
sig do
69+
params(
70+
image_source: String,
71+
details: T::Hash[String, T.untyped]
72+
).returns(T.nilable(Dependabot::Source))
73+
end
74+
def build_source_from_image_version(image_source, details)
75+
image_version = details.dig("config", "Labels", "org.opencontainers.image.version")
76+
revision = details.dig("config", "Labels", "org.opencontainers.image.revision")
77+
# Sometimes the versions are not tags (e.g., "24.04")
78+
# We only want to build a source if the version looks like a tag (starts with "v")
79+
# This is a safeguard for a first iteration. We may adjust this later based on user feedback.
80+
tag_like = image_version&.start_with?("v")
81+
82+
return unless tag_like || revision
83+
84+
parsed_source = Dependabot::Source.from_url(image_source)
85+
return unless parsed_source
86+
87+
branch_info = image_version ? "image version '#{image_version}'" : "unknown image version"
88+
commit_info = revision ? "revision '#{revision}'" : "no commit"
89+
Dependabot.logger.info "Building source with #{branch_info} and #{commit_info}"
90+
91+
Dependabot::Source.new(
92+
provider: parsed_source.provider,
93+
repo: parsed_source.repo,
94+
directory: parsed_source.directory,
95+
branch: image_version,
96+
commit: revision
97+
)
98+
end
3499
end
35100
end
36101
end

docker/spec/dependabot/docker/metadata_finder_spec.rb

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,53 @@
5555
end
5656
end
5757

58+
context "with a docker image with both tag and sha that has an OCI source annotation" do
59+
let(:dependency_with_tag_and_sha_source) do
60+
Dependabot::Dependency.new(
61+
name: "dependabot-fixtures/docker-with-source",
62+
version: "v0.0.2",
63+
requirements: [{
64+
file: "Dockerfile",
65+
requirement: nil,
66+
groups: [],
67+
source: { registry: "ghcr.io",
68+
digest: "389a5a9a5457ed237b05d623ddc31a42fa97811051dcd02d7ca4ad46bd3edd3e",
69+
tag: "v0.0.2" }
70+
}],
71+
package_manager: "docker"
72+
)
73+
end
74+
75+
let(:dependency) { dependency_with_tag_and_sha_source }
76+
77+
it "finds the repository" do
78+
expect(finder.source_url).to eq "https://github.com/dependabot-fixtures/docker-with-source"
79+
end
80+
end
81+
82+
context "with a digest but no tag or revision data" do
83+
let(:dependency_with_sha_no_tag) do
84+
Dependabot::Dependency.new(
85+
name: "dependabot/dependabot-updater-npm",
86+
version: "",
87+
requirements: [{
88+
file: "Dockerfile",
89+
requirement: nil,
90+
groups: [],
91+
source: { registry: "ghcr.io",
92+
digest: "74c21f5886502d754c47a163975062e0d3065e3d19f43c8f48c9dbeb2126767e" }
93+
}],
94+
package_manager: "docker"
95+
)
96+
end
97+
98+
let(:dependency) { dependency_with_sha_no_tag }
99+
100+
it "does not find the repository" do
101+
expect(finder.source_url).to be_nil
102+
end
103+
end
104+
58105
context "with a docker image that lacks an OCI source annotation" do
59106
let(:dependency) { dependency_without_source }
60107

@@ -73,7 +120,57 @@
73120
requirement: nil,
74121
groups: [],
75122
source: { registry: "ghcr.io",
76-
digest: "sha256:389a5a9a5457ed237b05d623ddc31a42fa97811051dcd02d7ca4ad46bd3edd3e" }
123+
digest: "389a5a9a5457ed237b05d623ddc31a42fa97811051dcd02d7ca4ad46bd3edd3e" }
124+
}],
125+
package_manager: "docker"
126+
)
127+
end
128+
129+
it "doesn't find the repository" do
130+
expect(finder.source_url).to be_nil
131+
end
132+
end
133+
134+
context "with a docker image without a tag but with org.opencontainers.image.version populated" do
135+
let(:dependency) do
136+
Dependabot::Dependency.new(
137+
name: "regclient/regctl",
138+
version: "",
139+
requirements: [{
140+
file: "Dockerfile",
141+
requirement: nil,
142+
groups: [],
143+
source: { registry: "ghcr.io",
144+
digest: "a734f285c0962e46557bff24489fa0b0521455733f72d9eb30c4f7a5027aeed6" }
145+
}],
146+
package_manager: "docker"
147+
)
148+
end
149+
150+
it "finds the repository" do
151+
expect(finder.source_url).to eq "https://github.com/regclient/regclient"
152+
# Normally, accessing private methods in tests is discouraged.
153+
# In this case, we need to verify the branch and commit derived from the image within the source
154+
# to ensure the source construction logic is correct. This access is for internal validation only.
155+
# Exposing the source publicly only for this test would be less desirable.
156+
expect(finder.send(:source).branch).to eq "v0.11.1"
157+
expect(finder.send(:source).commit).to eq "bf3bcfc47173b49ee8000d1d3a1ac15036e83cf0"
158+
end
159+
end
160+
161+
context "with a docker image without a tag but without a proper tag format or revision" do
162+
# The image used here has org.opencontainers.image.version set to "24.04"
163+
# which refers to the Ubuntu version rather than a tag
164+
let(:dependency) do
165+
Dependabot::Dependency.new(
166+
name: "maven",
167+
version: "",
168+
requirements: [{
169+
file: "Dockerfile",
170+
requirement: nil,
171+
groups: [],
172+
source: { registry: "docker.io",
173+
digest: "800a33a4cb190082c47abcd57944c852e1dece834f92c0aef65bea6336c52a72" }
77174
}],
78175
package_manager: "docker"
79176
)

0 commit comments

Comments
 (0)