secupdater 0.2.0alpha4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/secupdater/ecosystem/rubygems.rb +19 -1
- data/lib/secupdater/publisher/gitlab.rb +43 -2
- data/lib/secupdater/publisher.rb +7 -0
- data/lib/secupdater/update.rb +5 -1
- data/lib/secupdater/version.rb +1 -1
- data/spec/e2e/secupdater_spec.rb +11 -1
- data/spec/secupdater/ecosystem/rubygems_spec.rb +65 -11
- data/spec/secupdater/publisher/gitlab_spec.rb +73 -1
- data/spec/secupdater/update_spec.rb +25 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3e2a3d2fd687a912cd17a2dab8216d5c7187693c5239ccfdeac56ce87a825ac
|
|
4
|
+
data.tar.gz: 6db3dd9e3f87b76f96e75b3849c9d447ac24a1f0ea31972d24a37cdc20437ee6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66aabee38119a94edf9f609ba05d6d8c59581a7994f3f8c3a089f836c8460af124a5a34b08f44418dd9588439b590204864970fad3faf492e05a885f605ae329
|
|
7
|
+
data.tar.gz: 3f8607eca5779819b70d6346365dfa70ec158af70bc9f5c8a3d6c2c3ea9bef3a53839708c9eee388ff942369cc5303c03af958db07013dfb9d85ec04d162155a
|
|
@@ -35,7 +35,7 @@ module Secupdater
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def run_update(names, level)
|
|
38
|
-
output, status = Open3.capture2e(bundle_env, "bundle", "lock", "--update", *Array(names), "--conservative", "--#{level}", "--strict")
|
|
38
|
+
output, status = Open3.capture2e(bundle_env, "bundle", *bundler_version_arg, "lock", "--update", *Array(names), "--conservative", "--#{level}", "--strict")
|
|
39
39
|
|
|
40
40
|
if status.exitstatus == FROZEN_EXIT_CODE
|
|
41
41
|
raise Secupdater::Error, "bundle lock can't change Gemfile.lock (is BUNDLE_DEPLOYMENT set?)\n#{output}"
|
|
@@ -79,6 +79,24 @@ module Secupdater
|
|
|
79
79
|
|
|
80
80
|
private
|
|
81
81
|
|
|
82
|
+
def bundler_version_arg
|
|
83
|
+
version = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock")).bundler_version
|
|
84
|
+
return [] unless version
|
|
85
|
+
|
|
86
|
+
install_bundler(version)
|
|
87
|
+
["_#{version}_"]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def install_bundler(version)
|
|
91
|
+
@installed_bundlers ||= Set.new
|
|
92
|
+
return if @installed_bundlers.include?(version)
|
|
93
|
+
|
|
94
|
+
output, status = Open3.capture2e(bundle_env, "gem", "install", "bundler", "--version", version.to_s, "--conservative", "--no-document")
|
|
95
|
+
raise Secupdater::Error, "Couldn't install Bundler #{version}, which Gemfile.lock was bundled with\n#{output}" unless status.success?
|
|
96
|
+
|
|
97
|
+
@installed_bundlers << version
|
|
98
|
+
end
|
|
99
|
+
|
|
82
100
|
# When secupdater itself runs under `bundle exec` (e.g. an image whose
|
|
83
101
|
# entrypoint is `bundle exec`), undo what that set up so bundle lock works on
|
|
84
102
|
# the Gemfile and Gemfile.lock here. PATH is left alone, as callers may have
|
|
@@ -11,6 +11,7 @@ module Secupdater
|
|
|
11
11
|
@project_id = project_id
|
|
12
12
|
@target_branch = target_branch
|
|
13
13
|
@start_sha = start_sha
|
|
14
|
+
@current_merge_requests = {}
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
# Added to every MR secupdater opens. Closing one declines that version of
|
|
@@ -36,13 +37,39 @@ module Secupdater
|
|
|
36
37
|
commit(http)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
if open_merge_request
|
|
41
|
+
remember(open_merge_request)
|
|
42
|
+
return [:up_to_date, nil] if up_to_date
|
|
43
|
+
else
|
|
44
|
+
remember(JSON.parse(create_merge_request(http).body))
|
|
45
|
+
end
|
|
40
46
|
|
|
41
|
-
create_merge_request(http) unless open_merge_request
|
|
42
47
|
[:published, nil]
|
|
43
48
|
end
|
|
44
49
|
end
|
|
45
50
|
|
|
51
|
+
# Closes open secupdater MRs for a dependency that this run opened or kept a
|
|
52
|
+
# different MR for, or that no longer has any advisories. MRs for
|
|
53
|
+
# dependencies this run couldn't update are left alone.
|
|
54
|
+
def finish(vulnerable_names)
|
|
55
|
+
Net::HTTP.start("gitlab.com", 443, use_ssl: true) do |http|
|
|
56
|
+
find_open_secupdater_merge_requests(http).filter_map do |merge_request|
|
|
57
|
+
name = merge_request["source_branch"][REFERENCE_PATTERN, :name]
|
|
58
|
+
current = @current_merge_requests[name]
|
|
59
|
+
|
|
60
|
+
reason = if current && current != merge_request["iid"]
|
|
61
|
+
"Superseded by !#{current}."
|
|
62
|
+
elsif name && !vulnerable_names.include?(name)
|
|
63
|
+
"#{name} no longer has any security advisories on #{target_branch}, so this is no longer needed."
|
|
64
|
+
end
|
|
65
|
+
next unless reason
|
|
66
|
+
|
|
67
|
+
close_merge_request(http, merge_request["iid"], reason)
|
|
68
|
+
"closed !#{merge_request["iid"]} (#{merge_request["source_branch"]}): #{reason}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
46
73
|
private
|
|
47
74
|
|
|
48
75
|
def commit(http, force: false)
|
|
@@ -57,6 +84,20 @@ module Secupdater
|
|
|
57
84
|
request(http, Net::HTTP::Post, "repository/commits", data)
|
|
58
85
|
end
|
|
59
86
|
|
|
87
|
+
def remember(merge_request)
|
|
88
|
+
@current_merge_requests[bump.dependency.name] = merge_request["iid"]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def find_open_secupdater_merge_requests(http)
|
|
92
|
+
query = URI.encode_www_form(state: "opened", labels: LABEL, target_branch: target_branch, per_page: 100)
|
|
93
|
+
JSON.parse(request(http, Net::HTTP::Get, "merge_requests?#{query}").body)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def close_merge_request(http, iid, reason)
|
|
97
|
+
request(http, Net::HTTP::Post, "merge_requests/#{iid}/notes", { body: reason })
|
|
98
|
+
request(http, Net::HTTP::Put, "merge_requests/#{iid}", { state_event: "close" })
|
|
99
|
+
end
|
|
100
|
+
|
|
60
101
|
def create_merge_request(http)
|
|
61
102
|
data = {
|
|
62
103
|
source_branch: reference,
|
data/lib/secupdater/publisher.rb
CHANGED
|
@@ -17,6 +17,13 @@ module Secupdater
|
|
|
17
17
|
run
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# Called once every dependency has been through #call, with the names of all
|
|
21
|
+
# the dependencies that have advisories. Returns messages about anything it
|
|
22
|
+
# tidied up.
|
|
23
|
+
def finish(vulnerable_names) = []
|
|
24
|
+
|
|
25
|
+
REFERENCE_PATTERN = %r{\Asecurity/(?<name>.+)-(?<version>[^-]+)\z}
|
|
26
|
+
|
|
20
27
|
def reference = "security/#{bump.dependency.name}-#{bump.to}"
|
|
21
28
|
|
|
22
29
|
def title = "Upgrade #{ecosystem.ecosystem_name} dependency #{bump.dependency.name} to version #{bump.to}"
|
data/lib/secupdater/update.rb
CHANGED
|
@@ -16,9 +16,13 @@ module Secupdater
|
|
|
16
16
|
missing = deps_needing_checking.keys - original.keys
|
|
17
17
|
raise Secupdater::Error, "Advisories for dependencies not in the lockfile: #{missing.join(', ')}" if missing.any?
|
|
18
18
|
|
|
19
|
-
deps_needing_checking.to_h do |name, dep_advisories|
|
|
19
|
+
results = deps_needing_checking.to_h do |name, dep_advisories|
|
|
20
20
|
[name, update_dependency(original[name], dep_advisories)]
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
publisher&.finish(deps_needing_checking.keys)&.each { logger.info it }
|
|
24
|
+
|
|
25
|
+
results
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
def update_dependency(dep, advisories)
|
data/lib/secupdater/version.rb
CHANGED
data/spec/e2e/secupdater_spec.rb
CHANGED
|
@@ -54,6 +54,8 @@ RSpec.describe "secupdater end to end" do
|
|
|
54
54
|
FileUtils.mkdir("bin")
|
|
55
55
|
File.write("bin/bundle", "#!/bin/sh\ncp Gemfile.lock.after Gemfile.lock\n")
|
|
56
56
|
File.chmod(0o755, "bin/bundle")
|
|
57
|
+
File.write("bin/gem", "#!/bin/sh\n")
|
|
58
|
+
File.chmod(0o755, "bin/gem")
|
|
57
59
|
|
|
58
60
|
system("git init -q && git add Gemfile.lock && git -c user.name=test -c user.email=test@example.com commit -qm init", exception: true)
|
|
59
61
|
|
|
@@ -70,7 +72,14 @@ RSpec.describe "secupdater end to end" do
|
|
|
70
72
|
let(:env) { { "SECUPDATER_GITLAB_TOKEN" => "token", "CI_PROJECT_ID" => "123", "CI_DEFAULT_BRANCH" => "main" } }
|
|
71
73
|
|
|
72
74
|
let!(:find_mr_stub) do
|
|
73
|
-
stub_request(:get,
|
|
75
|
+
stub_request(:get, "https://gitlab.com/api/v4/projects/123/merge_requests")
|
|
76
|
+
.with(query: hash_including(source_branch: "security/css_parser-1.1.0"))
|
|
77
|
+
.to_return(status: 200, body: "[]")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
let!(:find_open_secupdater_mrs_stub) do
|
|
81
|
+
stub_request(:get, "https://gitlab.com/api/v4/projects/123/merge_requests")
|
|
82
|
+
.with(query: hash_including(state: "opened", labels: "secupdater"))
|
|
74
83
|
.to_return(status: 200, body: "[]")
|
|
75
84
|
end
|
|
76
85
|
|
|
@@ -99,6 +108,7 @@ RSpec.describe "secupdater end to end" do
|
|
|
99
108
|
expect { run_cli }.not_to raise_error
|
|
100
109
|
|
|
101
110
|
expect(find_mr_stub).to have_been_requested.once
|
|
111
|
+
expect(find_open_secupdater_mrs_stub).to have_been_requested.once
|
|
102
112
|
expect(create_commit_stub).to have_been_requested.once
|
|
103
113
|
expect(create_mr_stub).to have_been_requested.once
|
|
104
114
|
expect(File.read("Gemfile.lock")).to eq(lockfile("1.0.0"))
|
|
@@ -145,11 +145,65 @@ RSpec.describe Secupdater::Ecosystem::Rubygems do
|
|
|
145
145
|
}
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
let(:install_status) { instance_double(Process::Status, success?: true, exitstatus: 0) }
|
|
149
|
+
|
|
150
|
+
before do
|
|
151
|
+
allow(Bundler).to receive(:original_env).and_return(original_env)
|
|
152
|
+
allow(Open3).to receive(:capture2e).with(anything, "gem", any_args).and_return(["", install_status])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
context "lockfile bundled with a Bundler other than the one running" do
|
|
156
|
+
before { allow(Open3).to receive(:capture2e).with(anything, "bundle", any_args).and_return(["", status]) }
|
|
157
|
+
|
|
158
|
+
it "installs that Bundler if it's missing, then runs bundle lock with it" do
|
|
159
|
+
rubygems.try_update("css_parser", level: :patch)
|
|
160
|
+
|
|
161
|
+
expect(Open3).to have_received(:capture2e).with(bundle_env, "gem", "install", "bundler", "--version", "2.5.0", "--conservative", "--no-document").ordered
|
|
162
|
+
expect(Open3).to have_received(:capture2e).with(bundle_env, "bundle", "_2.5.0_", "lock", any_args).ordered
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "only installs it once per run" do
|
|
166
|
+
rubygems.try_update("css_parser", level: :patch)
|
|
167
|
+
rubygems.try_update("css_parser", level: :minor)
|
|
168
|
+
|
|
169
|
+
expect(Open3).to have_received(:capture2e).with(anything, "gem", any_args).once
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
context "lockfile without BUNDLED WITH" do
|
|
174
|
+
before do
|
|
175
|
+
File.write("Gemfile.lock", lockfile("1.0.0").sub(/\nBUNDLED WITH\n.*\n\z/, ""))
|
|
176
|
+
allow(Open3).to receive(:capture2e).with(anything, "bundle", any_args).and_return(["", status])
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "runs bundle lock with whichever Bundler RubyGems picks, without installing one" do
|
|
180
|
+
rubygems.try_update("css_parser", level: :patch)
|
|
181
|
+
|
|
182
|
+
expect(Open3).not_to have_received(:capture2e).with(anything, "gem", any_args)
|
|
183
|
+
expect(Open3).to have_received(:capture2e).with(bundle_env, "bundle", "lock", "--update", "css_parser", "--conservative", "--patch", "--strict")
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
context "locked Bundler can't be installed" do
|
|
188
|
+
let(:install_status) { instance_double(Process::Status, success?: false, exitstatus: 2) }
|
|
189
|
+
|
|
190
|
+
before do
|
|
191
|
+
allow(Open3).to receive(:capture2e).with(anything, "gem", any_args).and_return(["Could not find a valid gem 'bundler' (= 2.5.0)", install_status])
|
|
192
|
+
allow(Open3).to receive(:capture2e).with(anything, "bundle", any_args)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "raises Secupdater::Error rather than UpdateCommandFailed, so the run stops, and doesn't run bundle lock" do
|
|
196
|
+
expect { rubygems.try_update("css_parser", level: :patch) }.to raise_error(Secupdater::Error) do
|
|
197
|
+
expect(it).not_to be_a(Secupdater::UpdateCommandFailed)
|
|
198
|
+
expect(it.message).to include("Bundler 2.5.0", "Could not find a valid gem")
|
|
199
|
+
end
|
|
200
|
+
expect(Open3).not_to have_received(:capture2e).with(anything, "bundle", any_args)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
149
203
|
|
|
150
204
|
context "command succeeds" do
|
|
151
205
|
before do
|
|
152
|
-
allow(Open3).to receive(:capture2e) do
|
|
206
|
+
allow(Open3).to receive(:capture2e).with(anything, "bundle", any_args) do
|
|
153
207
|
File.write("Gemfile.lock", lockfile("1.1.0"))
|
|
154
208
|
["Bundle updated!", status]
|
|
155
209
|
end
|
|
@@ -158,7 +212,7 @@ RSpec.describe Secupdater::Ecosystem::Rubygems do
|
|
|
158
212
|
it "runs a strict, conservative, unfrozen bundle lock --update at the given level" do
|
|
159
213
|
rubygems.try_update("css_parser", level: :minor)
|
|
160
214
|
|
|
161
|
-
expect(Open3).to have_received(:capture2e).with(bundle_env, "bundle", "lock", "--update", "css_parser", "--conservative", "--minor", "--strict")
|
|
215
|
+
expect(Open3).to have_received(:capture2e).with(bundle_env, "bundle", "_2.5.0_", "lock", "--update", "css_parser", "--conservative", "--minor", "--strict")
|
|
162
216
|
end
|
|
163
217
|
|
|
164
218
|
it "returns the lockfile change with its new and original content" do
|
|
@@ -169,7 +223,7 @@ RSpec.describe Secupdater::Ecosystem::Rubygems do
|
|
|
169
223
|
end
|
|
170
224
|
|
|
171
225
|
context "command succeeds without changing anything" do
|
|
172
|
-
before { allow(Open3).to receive(:capture2e).and_return(["Bundle updated!", status]) }
|
|
226
|
+
before { allow(Open3).to receive(:capture2e).with(anything, "bundle", any_args).and_return(["Bundle updated!", status]) }
|
|
173
227
|
|
|
174
228
|
it "returns no file changes" do
|
|
175
229
|
expect(rubygems.try_update("css_parser", level: :patch)).to eq([])
|
|
@@ -177,12 +231,12 @@ RSpec.describe Secupdater::Ecosystem::Rubygems do
|
|
|
177
231
|
end
|
|
178
232
|
|
|
179
233
|
context "gem name containing shell metacharacters" do
|
|
180
|
-
before { allow(Open3).to receive(:capture2e).and_return(["", status]) }
|
|
234
|
+
before { allow(Open3).to receive(:capture2e).with(anything, "bundle", any_args).and_return(["", status]) }
|
|
181
235
|
|
|
182
236
|
it "passes it as a single argument without going through a shell" do
|
|
183
237
|
rubygems.try_update("css_parser; touch pwned", level: :patch)
|
|
184
238
|
|
|
185
|
-
expect(Open3).to have_received(:capture2e).with(bundle_env, "bundle", "lock", "--update", "css_parser; touch pwned", "--conservative", "--patch", "--strict")
|
|
239
|
+
expect(Open3).to have_received(:capture2e).with(bundle_env, "bundle", "_2.5.0_", "lock", "--update", "css_parser; touch pwned", "--conservative", "--patch", "--strict")
|
|
186
240
|
end
|
|
187
241
|
end
|
|
188
242
|
|
|
@@ -196,14 +250,14 @@ RSpec.describe Secupdater::Ecosystem::Rubygems do
|
|
|
196
250
|
}
|
|
197
251
|
end
|
|
198
252
|
|
|
199
|
-
before { allow(Open3).to receive(:capture2e).and_return(["", status]) }
|
|
253
|
+
before { allow(Open3).to receive(:capture2e).with(anything, "bundle", any_args).and_return(["", status]) }
|
|
200
254
|
|
|
201
255
|
it "runs bundle lock with Bundler's settings from before bundle exec, on the Gemfile in the working directory" do
|
|
202
256
|
rubygems.try_update("css_parser", level: :patch)
|
|
203
257
|
|
|
204
258
|
expect(Open3).to have_received(:capture2e).with(
|
|
205
259
|
bundle_env.merge("GEM_HOME" => "/usr/local/bundle", "RUBYOPT" => "-W0"),
|
|
206
|
-
"bundle", "lock", "--update", "css_parser", "--conservative", "--patch", "--strict"
|
|
260
|
+
"bundle", "_2.5.0_", "lock", "--update", "css_parser", "--conservative", "--patch", "--strict"
|
|
207
261
|
)
|
|
208
262
|
end
|
|
209
263
|
end
|
|
@@ -211,7 +265,7 @@ RSpec.describe Secupdater::Ecosystem::Rubygems do
|
|
|
211
265
|
context "command exits 1" do
|
|
212
266
|
let(:status) { instance_double(Process::Status, success?: false, exitstatus: 1) }
|
|
213
267
|
|
|
214
|
-
before { allow(Open3).to receive(:capture2e).and_return(["Could not find gem 'css_parser'", status]) }
|
|
268
|
+
before { allow(Open3).to receive(:capture2e).with(anything, "bundle", any_args).and_return(["Could not find gem 'css_parser'", status]) }
|
|
215
269
|
|
|
216
270
|
it "raises a retryable UpdateCommandFailed with the integer exit code and combined output" do
|
|
217
271
|
expect { rubygems.try_update("css_parser", level: :patch) }.to raise_error(Secupdater::UpdateCommandFailed) do
|
|
@@ -233,7 +287,7 @@ RSpec.describe Secupdater::Ecosystem::Rubygems do
|
|
|
233
287
|
context "command exits #{exit_code} (#{reason})" do
|
|
234
288
|
let(:status) { instance_double(Process::Status, success?: false, exitstatus: exit_code) }
|
|
235
289
|
|
|
236
|
-
before { allow(Open3).to receive(:capture2e).and_return([reason, status]) }
|
|
290
|
+
before { allow(Open3).to receive(:capture2e).with(anything, "bundle", any_args).and_return([reason, status]) }
|
|
237
291
|
|
|
238
292
|
it "raises UnretryableUpdateFailed with the exit code and output" do
|
|
239
293
|
expect { rubygems.try_update("css_parser", level: :patch) }.to raise_error(Secupdater::UnretryableUpdateFailed) do
|
|
@@ -247,7 +301,7 @@ RSpec.describe Secupdater::Ecosystem::Rubygems do
|
|
|
247
301
|
context "command exits 16 (frozen lockfile)" do
|
|
248
302
|
let(:status) { instance_double(Process::Status, success?: false, exitstatus: 16) }
|
|
249
303
|
|
|
250
|
-
before { allow(Open3).to receive(:capture2e).and_return(["frozen mode is set", status]) }
|
|
304
|
+
before { allow(Open3).to receive(:capture2e).with(anything, "bundle", any_args).and_return(["frozen mode is set", status]) }
|
|
251
305
|
|
|
252
306
|
it "raises Secupdater::Error rather than UpdateCommandFailed, so the run stops" do
|
|
253
307
|
expect { rubygems.try_update("css_parser", level: :patch) }.to raise_error(Secupdater::Error) do
|
|
@@ -86,7 +86,7 @@ RSpec.describe Secupdater::Publisher::Gitlab do
|
|
|
86
86
|
title: "Upgrade RubyGems dependency css_parser to version 1.1.0",
|
|
87
87
|
description: "Upgrade RubyGems dependency css_parser to version 1.1.0 to address the following security advisories:\n\n* cve/2024-0001\n",
|
|
88
88
|
}, headers: headers)
|
|
89
|
-
.to_return(status: 201, body:
|
|
89
|
+
.to_return(status: 201, body: { iid: 9 }.to_json)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def publish = gitlab.call(ecosystem, bump, advisories, changes)
|
|
@@ -243,4 +243,76 @@ RSpec.describe Secupdater::Publisher::Gitlab do
|
|
|
243
243
|
end
|
|
244
244
|
end
|
|
245
245
|
end
|
|
246
|
+
|
|
247
|
+
describe "#finish(vulnerable_names)" do
|
|
248
|
+
let(:open_secupdater_merge_requests) { [] }
|
|
249
|
+
|
|
250
|
+
let!(:find_open_stub) do
|
|
251
|
+
stub_request(:get, "#{project_api}/merge_requests")
|
|
252
|
+
.with(query: { state: "opened", labels: "secupdater", target_branch: "main", per_page: "100" }, headers: headers)
|
|
253
|
+
.to_return(status: 200, body: open_secupdater_merge_requests.to_json)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
let!(:note_stub) { stub_request(:post, %r{\A#{project_api}/merge_requests/\d+/notes\z}).to_return(status: 201, body: "{}") }
|
|
257
|
+
let!(:close_stub) do
|
|
258
|
+
stub_request(:put, %r{\A#{project_api}/merge_requests/\d+\z})
|
|
259
|
+
.with(body: { state_event: "close" })
|
|
260
|
+
.to_return(status: 200, body: "{}")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
context "an older MR for a dependency this run opened a new MR for" do
|
|
264
|
+
let(:open_secupdater_merge_requests) do
|
|
265
|
+
[
|
|
266
|
+
{ iid: 5, source_branch: "security/css_parser-1.0.5" },
|
|
267
|
+
{ iid: 9, source_branch: "security/css_parser-1.1.0" }
|
|
268
|
+
]
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
before { publish }
|
|
272
|
+
|
|
273
|
+
it "closes it, saying which MR superseded it" do
|
|
274
|
+
expect(gitlab.finish(["css_parser"])).to eq(["closed !5 (security/css_parser-1.0.5): Superseded by !9."])
|
|
275
|
+
|
|
276
|
+
expect(a_request(:post, "#{project_api}/merge_requests/5/notes").with(body: { body: "Superseded by !9." })).to have_been_made
|
|
277
|
+
expect(a_request(:put, "#{project_api}/merge_requests/5")).to have_been_made
|
|
278
|
+
expect(a_request(:put, //)).to have_been_made.once
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
context "an MR for a dependency that no longer has advisories" do
|
|
283
|
+
let(:open_secupdater_merge_requests) { [{ iid: 4, source_branch: "security/net-ssh-7.3.8" }] }
|
|
284
|
+
|
|
285
|
+
it "closes it, saying it's no longer needed" do
|
|
286
|
+
expect(gitlab.finish(["css_parser"])).to eq(
|
|
287
|
+
["closed !4 (security/net-ssh-7.3.8): net-ssh no longer has any security advisories on main, so this is no longer needed."]
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
expect(a_request(:put, "#{project_api}/merge_requests/4")).to have_been_made
|
|
291
|
+
expect(a_request(:put, //)).to have_been_made.once
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
context "an MR for a dependency that still has advisories, but that this run didn't publish" do
|
|
296
|
+
let(:open_secupdater_merge_requests) { [{ iid: 4, source_branch: "security/net-ssh-7.3.8" }] }
|
|
297
|
+
|
|
298
|
+
it "leaves it alone" do
|
|
299
|
+
expect(gitlab.finish(["net-ssh"])).to eq([])
|
|
300
|
+
|
|
301
|
+
expect(a_request(:put, //)).not_to have_been_made
|
|
302
|
+
expect(a_request(:post, //)).not_to have_been_made
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
context "the MR this run opened or kept" do
|
|
307
|
+
let(:open_secupdater_merge_requests) { [{ iid: 9, source_branch: "security/css_parser-1.1.0" }] }
|
|
308
|
+
|
|
309
|
+
before { publish }
|
|
310
|
+
|
|
311
|
+
it "leaves it alone" do
|
|
312
|
+
expect(gitlab.finish(["css_parser"])).to eq([])
|
|
313
|
+
|
|
314
|
+
expect(a_request(:put, //)).not_to have_been_made
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
246
318
|
end
|
|
@@ -85,6 +85,7 @@ RSpec.describe Secupdater::Update do
|
|
|
85
85
|
|
|
86
86
|
before do
|
|
87
87
|
allow(publisher).to receive(:call) { |*args| published << args; [:published, nil] } if publisher
|
|
88
|
+
allow(publisher).to receive(:finish).and_return([]) if publisher
|
|
88
89
|
end
|
|
89
90
|
|
|
90
91
|
describe "#call(advisories)" do
|
|
@@ -274,6 +275,30 @@ RSpec.describe Secupdater::Update do
|
|
|
274
275
|
end
|
|
275
276
|
end
|
|
276
277
|
|
|
278
|
+
context "once every gem has been through the publisher" do
|
|
279
|
+
let(:outcomes) { { "rack" => "2.1", "nokogiri" => :unchanged } }
|
|
280
|
+
|
|
281
|
+
before { allow(publisher).to receive(:finish).and_return(["closed !5 (security/rack-2.0): Superseded by !9."]) }
|
|
282
|
+
|
|
283
|
+
it "lets the publisher tidy up, with the names of every gem that has advisories" do
|
|
284
|
+
update.call([advisory("rack", "cve/1"), advisory("nokogiri", "cve/2")])
|
|
285
|
+
|
|
286
|
+
expect(publisher).to have_received(:finish).with(["rack", "nokogiri"])
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it "logs what it tidied up" do
|
|
290
|
+
update.call([advisory("rack", "cve/1")])
|
|
291
|
+
|
|
292
|
+
expect(log.string).to match(%r{^INFO closed !5 \(security/rack-2\.0\): Superseded by !9\.})
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it "does so even when there are no advisories, so MRs whose fixes have landed get closed" do
|
|
296
|
+
expect(update.call([])).to eq({})
|
|
297
|
+
|
|
298
|
+
expect(publisher).to have_received(:finish).with([])
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
277
302
|
context "publishing fails for the first gem" do
|
|
278
303
|
let(:outcomes) { { "rack" => "2.1", "nokogiri" => "2.0" } }
|
|
279
304
|
|