dependabot-docker_compose 0.297.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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