dependabot-docker_compose 0.297.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/dependabot/docker_compose/file_fetcher.rb +67 -0
- data/lib/dependabot/docker_compose/file_parser.rb +76 -0
- data/lib/dependabot/docker_compose/file_updater.rb +106 -0
- data/lib/dependabot/docker_compose/metadata_finder.rb +39 -0
- data/lib/dependabot/docker_compose/package_manager.rb +53 -0
- data/lib/dependabot/docker_compose/requirement.rb +43 -0
- data/lib/dependabot/docker_compose/tag.rb +144 -0
- data/lib/dependabot/docker_compose/update_checker.rb +479 -0
- data/lib/dependabot/docker_compose/version.rb +84 -0
- data/lib/dependabot/docker_compose.rb +19 -0
- data/lib/dependabot/shared/shared_file_fetcher.rb +99 -0
- data/lib/dependabot/shared/shared_file_parser.rb +80 -0
- data/lib/dependabot/shared/shared_file_updater.rb +261 -0
- data/lib/dependabot/shared/utils/credentials_finder.rb +101 -0
- data/lib/dependabot/shared/utils/helpers.rb +19 -0
- metadata +285 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
# typed: strong
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dependabot/dependency"
|
5
|
+
require "dependabot/file_parsers"
|
6
|
+
require "dependabot/file_parsers/base"
|
7
|
+
require "sorbet-runtime"
|
8
|
+
|
9
|
+
module Dependabot
|
10
|
+
module Shared
|
11
|
+
class SharedFileParser < Dependabot::FileParsers::Base
|
12
|
+
extend T::Sig
|
13
|
+
extend T::Helpers
|
14
|
+
|
15
|
+
abstract!
|
16
|
+
|
17
|
+
require "dependabot/file_parsers/base/dependency_set"
|
18
|
+
|
19
|
+
# Details of Docker regular expressions is at
|
20
|
+
# https://github.com/docker/distribution/blob/master/reference/regexp.go
|
21
|
+
DOMAIN_COMPONENT = /(?:[[:alnum:]]|[[:alnum:]][[[:alnum:]]-]*[[:alnum:]])/
|
22
|
+
DOMAIN = /(?:#{DOMAIN_COMPONENT}(?:\.#{DOMAIN_COMPONENT})+)/
|
23
|
+
REGISTRY = /(?<registry>#{DOMAIN}(?::\d+)?)/
|
24
|
+
|
25
|
+
NAME_COMPONENT = /(?:[a-z\d]+(?:(?:[._]|__|[-]*)[a-z\d]+)*)/
|
26
|
+
IMAGE = %r{(?<image>#{NAME_COMPONENT}(?:/#{NAME_COMPONENT})*)}
|
27
|
+
|
28
|
+
TAG = /:(?<tag>[\w][\w.-]{0,127})/
|
29
|
+
DIGEST = /@(?<digest>[^\s]+)/
|
30
|
+
NAME = /\s+AS\s+(?<name>[\w-]+)/
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
sig { params(parsed_line: T::Hash[String, T.nilable(String)]).returns(T.nilable(String)) }
|
35
|
+
def version_from(parsed_line)
|
36
|
+
parsed_line.fetch("tag") || parsed_line.fetch("digest")
|
37
|
+
end
|
38
|
+
|
39
|
+
sig { params(parsed_line: T::Hash[String, T.nilable(String)]).returns(T::Hash[String, T.nilable(String)]) }
|
40
|
+
def source_from(parsed_line)
|
41
|
+
source = {}
|
42
|
+
|
43
|
+
source[:registry] = parsed_line.fetch("registry") if parsed_line.fetch("registry")
|
44
|
+
source[:tag] = parsed_line.fetch("tag") if parsed_line.fetch("tag")
|
45
|
+
source[:digest] = parsed_line.fetch("digest") if parsed_line.fetch("digest")
|
46
|
+
|
47
|
+
source
|
48
|
+
end
|
49
|
+
|
50
|
+
sig do
|
51
|
+
params(file: Dependabot::DependencyFile, details: T::Hash[String, T.nilable(String)],
|
52
|
+
version: String).returns(Dependabot::Dependency)
|
53
|
+
end
|
54
|
+
def build_dependency(file, details, version)
|
55
|
+
Dependency.new(
|
56
|
+
name: T.must(details.fetch("image")),
|
57
|
+
version: version,
|
58
|
+
package_manager: package_manager,
|
59
|
+
requirements: [
|
60
|
+
requirement: nil,
|
61
|
+
groups: [],
|
62
|
+
file: file.name,
|
63
|
+
source: source_from(details)
|
64
|
+
]
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
sig { override.void }
|
71
|
+
def check_required_files; end
|
72
|
+
|
73
|
+
sig { abstract.returns(String) }
|
74
|
+
def package_manager; end
|
75
|
+
|
76
|
+
sig { abstract.returns(String) }
|
77
|
+
def file_type; end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dependabot/file_updaters"
|
5
|
+
require "dependabot/file_updaters/base"
|
6
|
+
require "dependabot/errors"
|
7
|
+
require "sorbet-runtime"
|
8
|
+
require "dependabot/shared/utils/helpers"
|
9
|
+
|
10
|
+
module Dependabot
|
11
|
+
module Shared
|
12
|
+
class SharedFileUpdater < Dependabot::FileUpdaters::Base
|
13
|
+
extend T::Sig
|
14
|
+
extend T::Helpers
|
15
|
+
|
16
|
+
abstract!
|
17
|
+
|
18
|
+
FROM_REGEX = /FROM(\s+--platform\=\S+)?/i
|
19
|
+
|
20
|
+
sig { override.returns(T::Array[Dependabot::DependencyFile]) }
|
21
|
+
def updated_dependency_files
|
22
|
+
updated_files = []
|
23
|
+
dependency_files.each do |file|
|
24
|
+
next unless requirement_changed?(file, T.must(dependency))
|
25
|
+
|
26
|
+
updated_files << if file.name.match?(T.must(yaml_file_pattern))
|
27
|
+
updated_file(
|
28
|
+
file: file,
|
29
|
+
content: T.must(updated_yaml_content(file))
|
30
|
+
)
|
31
|
+
else
|
32
|
+
updated_file(
|
33
|
+
file: file,
|
34
|
+
content: T.must(updated_dockerfile_content(file))
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
updated_files.reject! { |f| dependency_files.include?(f) }
|
40
|
+
raise "No files changed!" if updated_files.none?
|
41
|
+
|
42
|
+
updated_files
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { abstract.returns(T.nilable(Regexp)) }
|
46
|
+
def yaml_file_pattern; end
|
47
|
+
|
48
|
+
sig { abstract.returns(T.nilable(Regexp)) }
|
49
|
+
def container_image_regex; end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) }
|
54
|
+
def updated_dockerfile_content(file)
|
55
|
+
old_sources = previous_sources(file)
|
56
|
+
new_sources = sources(file)
|
57
|
+
|
58
|
+
updated_content = T.let(file.content, T.untyped)
|
59
|
+
|
60
|
+
T.must(old_sources).zip(new_sources).each do |old_source, new_source|
|
61
|
+
updated_content = update_digest_and_tag(updated_content, old_source, T.must(new_source))
|
62
|
+
end
|
63
|
+
|
64
|
+
raise "Expected content to change!" if updated_content == file.content
|
65
|
+
|
66
|
+
updated_content
|
67
|
+
end
|
68
|
+
|
69
|
+
sig do
|
70
|
+
params(previous_content: String, old_source: T::Hash[Symbol, T.nilable(String)],
|
71
|
+
new_source: T::Hash[Symbol, T.nilable(String)]).returns(String)
|
72
|
+
end
|
73
|
+
def update_digest_and_tag(previous_content, old_source, new_source)
|
74
|
+
old_digest = old_source[:digest]
|
75
|
+
new_digest = new_source[:digest]
|
76
|
+
|
77
|
+
old_tag = old_source[:tag]
|
78
|
+
new_tag = new_source[:tag]
|
79
|
+
|
80
|
+
old_declaration =
|
81
|
+
if private_registry_url(old_source)
|
82
|
+
"#{private_registry_url(old_source)}/"
|
83
|
+
else
|
84
|
+
""
|
85
|
+
end
|
86
|
+
old_declaration += T.must(dependency).name
|
87
|
+
old_declaration +=
|
88
|
+
if specified_with_tag?(old_source)
|
89
|
+
":#{old_tag}"
|
90
|
+
else
|
91
|
+
""
|
92
|
+
end
|
93
|
+
old_declaration +=
|
94
|
+
if specified_with_digest?(old_source)
|
95
|
+
"@sha256:#{old_digest}"
|
96
|
+
else
|
97
|
+
""
|
98
|
+
end
|
99
|
+
|
100
|
+
escaped_declaration = Regexp.escape(old_declaration)
|
101
|
+
|
102
|
+
old_declaration_regex = build_old_declaration_regex(escaped_declaration)
|
103
|
+
|
104
|
+
previous_content.gsub(old_declaration_regex) do |old_dec|
|
105
|
+
old_dec
|
106
|
+
.gsub("@sha256:#{old_digest}", "@sha256:#{new_digest}")
|
107
|
+
.gsub(":#{old_tag}", ":#{new_tag}")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
sig { params(escaped_declaration: String).returns(Regexp) }
|
112
|
+
def build_old_declaration_regex(escaped_declaration)
|
113
|
+
%r{^#{FROM_REGEX}\s+(docker\.io/)?#{escaped_declaration}(?=\s|$)}
|
114
|
+
end
|
115
|
+
|
116
|
+
sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) }
|
117
|
+
def updated_yaml_content(file)
|
118
|
+
updated_content = file.content
|
119
|
+
updated_content = update_helm(file, updated_content) if Shared::Utils.likely_helm_chart?(file)
|
120
|
+
updated_content = update_image(file, updated_content)
|
121
|
+
|
122
|
+
raise "Expected content to change!" if updated_content == file.content
|
123
|
+
|
124
|
+
updated_content
|
125
|
+
end
|
126
|
+
|
127
|
+
sig { params(file: Dependabot::DependencyFile, content: T.nilable(String)).returns(T.nilable(String)) }
|
128
|
+
def update_helm(file, content)
|
129
|
+
old_tags = old_helm_tags(file)
|
130
|
+
return if old_tags.empty?
|
131
|
+
|
132
|
+
modified_content = content
|
133
|
+
|
134
|
+
old_tags.each do |old_tag|
|
135
|
+
old_tag_regex = /^\s*(?:-\s)?(?:tag|version):\s+["']?#{old_tag}["']?(?=\s|$)/
|
136
|
+
modified_content = modified_content&.gsub(old_tag_regex) do |old_img_tag|
|
137
|
+
old_img_tag.gsub(old_tag.to_s, new_helm_tag(file).to_s)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
modified_content
|
141
|
+
end
|
142
|
+
|
143
|
+
sig { params(file: Dependabot::DependencyFile, content: T.nilable(String)).returns(T.nilable(String)) }
|
144
|
+
def update_image(file, content)
|
145
|
+
old_images = old_yaml_images(file)
|
146
|
+
return if old_images.empty?
|
147
|
+
|
148
|
+
modified_content = content
|
149
|
+
|
150
|
+
old_images.each do |old_image|
|
151
|
+
old_image_regex = /^\s*(?:-\s)?image:\s+#{old_image}(?=\s|$)/
|
152
|
+
modified_content = modified_content&.gsub(old_image_regex) do |old_img|
|
153
|
+
old_img.gsub(old_image.to_s, new_yaml_image(file).to_s)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
modified_content
|
157
|
+
end
|
158
|
+
|
159
|
+
sig { params(file: Dependabot::DependencyFile).returns(String) }
|
160
|
+
def new_yaml_image(file)
|
161
|
+
element = T.must(dependency).requirements.find { |r| r[:file] == file.name }
|
162
|
+
prefix = element&.dig(:source, :registry) ? "#{element.fetch(:source)[:registry]}/" : ""
|
163
|
+
digest = element&.dig(:source, :digest) ? "@sha256:#{element.fetch(:source)[:digest]}" : ""
|
164
|
+
tag = element&.dig(:source, :tag) ? ":#{element.fetch(:source)[:tag]}" : ""
|
165
|
+
"#{prefix}#{T.must(dependency).name}#{tag}#{digest}"
|
166
|
+
end
|
167
|
+
|
168
|
+
sig { params(file: Dependabot::DependencyFile).returns(T::Array[String]) }
|
169
|
+
def old_yaml_images(file)
|
170
|
+
T.must(previous_requirements(file)).map do |r|
|
171
|
+
prefix = r.fetch(:source)[:registry] ? "#{r.fetch(:source)[:registry]}/" : ""
|
172
|
+
digest = r.fetch(:source)[:digest] ? "@sha256:#{r.fetch(:source)[:digest]}" : ""
|
173
|
+
tag = r.fetch(:source)[:tag] ? ":#{r.fetch(:source)[:tag]}" : ""
|
174
|
+
"#{prefix}#{T.must(dependency).name}#{tag}#{digest}"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
sig { params(file: Dependabot::DependencyFile).returns(T::Array[String]) }
|
179
|
+
def old_helm_tags(file)
|
180
|
+
T.must(previous_requirements(file)).map do |r|
|
181
|
+
tag = r.fetch(:source)[:tag] || ""
|
182
|
+
digest = r.fetch(:source)[:digest] ? "@sha256:#{r.fetch(:source)[:digest]}" : ""
|
183
|
+
"#{tag}#{digest}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
sig { params(file: Dependabot::DependencyFile).returns(String) }
|
188
|
+
def new_helm_tag(file)
|
189
|
+
element = T.must(dependency).requirements.find { |r| r[:file] == file.name }
|
190
|
+
tag = T.must(element).dig(:source, :tag) || ""
|
191
|
+
digest = T.must(element).dig(:source, :digest) ? "@sha256:#{T.must(element).dig(:source, :digest)}" : ""
|
192
|
+
"#{tag}#{digest}"
|
193
|
+
end
|
194
|
+
|
195
|
+
protected
|
196
|
+
|
197
|
+
sig { params(file: Dependabot::DependencyFile, dependency: Dependabot::Dependency).returns(T::Boolean) }
|
198
|
+
def requirement_changed?(file, dependency)
|
199
|
+
changed_requirements =
|
200
|
+
dependency.requirements - T.must(dependency.previous_requirements)
|
201
|
+
|
202
|
+
changed_requirements.any? { |f| f[:file] == file.name }
|
203
|
+
end
|
204
|
+
|
205
|
+
sig { params(source: T::Hash[Symbol, T.nilable(String)]).returns(T::Boolean) }
|
206
|
+
def specified_with_tag?(source)
|
207
|
+
!source[:tag].nil?
|
208
|
+
end
|
209
|
+
|
210
|
+
sig { params(source: T::Hash[Symbol, T.nilable(String)]).returns(T::Boolean) }
|
211
|
+
def specified_with_digest?(source)
|
212
|
+
!source[:digest].nil?
|
213
|
+
end
|
214
|
+
|
215
|
+
sig { params(file: Dependabot::DependencyFile).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
216
|
+
def requirements(file)
|
217
|
+
T.must(dependency).requirements
|
218
|
+
.select { |r| r[:file] == file.name }
|
219
|
+
end
|
220
|
+
|
221
|
+
sig { params(file: Dependabot::DependencyFile).returns(T.nilable(T::Array[T::Hash[Symbol, T.untyped]])) }
|
222
|
+
def previous_requirements(file)
|
223
|
+
T.must(dependency).previous_requirements
|
224
|
+
&.select { |r| r[:file] == file.name }
|
225
|
+
end
|
226
|
+
|
227
|
+
sig { params(source: T::Hash[Symbol, T.nilable(String)]).returns(T.nilable(String)) }
|
228
|
+
def private_registry_url(source)
|
229
|
+
source[:registry]
|
230
|
+
end
|
231
|
+
|
232
|
+
sig { params(file: Dependabot::DependencyFile).returns(T::Array[T::Hash[Symbol, T.nilable(String)]]) }
|
233
|
+
def sources(file)
|
234
|
+
requirements(file).map { |r| r.fetch(:source) }
|
235
|
+
end
|
236
|
+
|
237
|
+
sig { params(file: Dependabot::DependencyFile).returns(T.nilable(T::Array[T::Hash[Symbol, T.nilable(String)]])) }
|
238
|
+
def previous_sources(file)
|
239
|
+
previous_requirements(file)&.map { |r| r.fetch(:source) }
|
240
|
+
end
|
241
|
+
|
242
|
+
sig { returns(T.nilable(Dependabot::Dependency)) }
|
243
|
+
def dependency
|
244
|
+
# Files will only ever be updating a single dependency
|
245
|
+
dependencies.first
|
246
|
+
end
|
247
|
+
|
248
|
+
sig { override.void }
|
249
|
+
def check_required_files
|
250
|
+
return if dependency_files.any?
|
251
|
+
|
252
|
+
raise "No #{file_type}!"
|
253
|
+
end
|
254
|
+
|
255
|
+
private
|
256
|
+
|
257
|
+
sig { abstract.returns(String) }
|
258
|
+
def file_type; end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "aws-sdk-ecr"
|
5
|
+
require "base64"
|
6
|
+
require "sorbet-runtime"
|
7
|
+
|
8
|
+
require "dependabot/credential"
|
9
|
+
require "dependabot/errors"
|
10
|
+
|
11
|
+
module Dependabot
|
12
|
+
module Shared
|
13
|
+
module Utils
|
14
|
+
class CredentialsFinder
|
15
|
+
extend T::Sig
|
16
|
+
|
17
|
+
AWS_ECR_URL = /dkr\.ecr\.(?<region>[^.]+)\.amazonaws\.com/
|
18
|
+
DEFAULT_DOCKER_HUB_REGISTRY = "registry.hub.docker.com"
|
19
|
+
|
20
|
+
sig { params(credentials: T::Array[Dependabot::Credential]).void }
|
21
|
+
def initialize(credentials)
|
22
|
+
@credentials = credentials
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { params(registry_hostname: T.nilable(String)).returns(T.nilable(Dependabot::Credential)) }
|
26
|
+
def credentials_for_registry(registry_hostname)
|
27
|
+
registry_details =
|
28
|
+
credentials
|
29
|
+
.select { |cred| cred["type"] == "docker_registry" }
|
30
|
+
.find { |cred| cred.fetch("registry") == registry_hostname }
|
31
|
+
return unless registry_details
|
32
|
+
return registry_details unless registry_hostname&.match?(AWS_ECR_URL)
|
33
|
+
|
34
|
+
build_aws_credentials(registry_details)
|
35
|
+
end
|
36
|
+
|
37
|
+
sig { returns(T.nilable(String)) }
|
38
|
+
def base_registry
|
39
|
+
@base_registry ||= T.let(
|
40
|
+
credentials.find do |cred|
|
41
|
+
cred["type"] == "docker_registry" && cred.replaces_base?
|
42
|
+
end,
|
43
|
+
T.nilable(Dependabot::Credential)
|
44
|
+
)
|
45
|
+
@base_registry ||= Dependabot::Credential.new({ "registry" => DEFAULT_DOCKER_HUB_REGISTRY,
|
46
|
+
"credentials" => nil })
|
47
|
+
@base_registry["registry"]
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { params(registry: String).returns(T::Boolean) }
|
51
|
+
def using_dockerhub?(registry)
|
52
|
+
registry == DEFAULT_DOCKER_HUB_REGISTRY
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
sig { returns(T::Array[Dependabot::Credential]) }
|
58
|
+
attr_reader :credentials
|
59
|
+
|
60
|
+
sig { params(registry_details: Dependabot::Credential).returns(Dependabot::Credential) }
|
61
|
+
def build_aws_credentials(registry_details)
|
62
|
+
# If credentials have been generated from AWS we can just return them
|
63
|
+
return registry_details if registry_details["username"] == "AWS"
|
64
|
+
|
65
|
+
# Build a client either with explicit creds or default creds
|
66
|
+
registry_hostname = registry_details.fetch("registry")
|
67
|
+
region = registry_hostname.match(AWS_ECR_URL).named_captures.fetch("region")
|
68
|
+
aws_credentials = Aws::Credentials.new(
|
69
|
+
registry_details["username"],
|
70
|
+
registry_details["password"]
|
71
|
+
)
|
72
|
+
|
73
|
+
ecr_client =
|
74
|
+
if aws_credentials.set?
|
75
|
+
Aws::ECR::Client.new(region: region, credentials: aws_credentials)
|
76
|
+
else
|
77
|
+
# Let the client check default locations for credentials
|
78
|
+
Aws::ECR::Client.new(region: region)
|
79
|
+
end
|
80
|
+
|
81
|
+
# If the client still lacks credentials, we might be running within GitHub's
|
82
|
+
# Dependabot Service, in which case we might get them from the proxy
|
83
|
+
return registry_details if ecr_client.config.credentials.nil?
|
84
|
+
|
85
|
+
# Otherwise, we need to use the provided Access Key ID and secret to
|
86
|
+
# generate a temporary username and password
|
87
|
+
@authorization_tokens ||= T.let({}, T.nilable(T::Hash[String, String]))
|
88
|
+
@authorization_tokens[registry_hostname] ||=
|
89
|
+
ecr_client.get_authorization_token.authorization_data.first.authorization_token
|
90
|
+
username, password =
|
91
|
+
Base64.decode64(T.must(@authorization_tokens[registry_hostname])).split(":")
|
92
|
+
registry_details.merge(Dependabot::Credential.new({ "username" => username, "password" => password }))
|
93
|
+
rescue Aws::Errors::MissingCredentialsError,
|
94
|
+
Aws::ECR::Errors::UnrecognizedClientException,
|
95
|
+
Aws::ECR::Errors::InvalidSignatureException
|
96
|
+
raise PrivateSourceAuthenticationFailure, registry_hostname
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# typed: strong
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Dependabot
|
7
|
+
module Shared
|
8
|
+
module Utils
|
9
|
+
HELM_REGEXP = /values[\-a-zA-Z_0-9]*\.ya?ml$/i
|
10
|
+
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { params(file: Dependabot::DependencyFile).returns(T::Boolean) }
|
14
|
+
def self.likely_helm_chart?(file)
|
15
|
+
file.name.match?(HELM_REGEXP)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|