secupdater 0.1.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 +7 -0
- data/.rspec +1 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +4 -0
- data/bin/secupdater +5 -0
- data/lib/secupdater/cli.rb +44 -0
- data/lib/secupdater/ecosystem/rubygems.rb +70 -0
- data/lib/secupdater/ecosystem.rb +43 -0
- data/lib/secupdater/publisher/gitlab.rb +67 -0
- data/lib/secupdater/publisher.rb +39 -0
- data/lib/secupdater/runner.rb +16 -0
- data/lib/secupdater/update.rb +85 -0
- data/lib/secupdater/version.rb +5 -0
- data/lib/secupdater.rb +43 -0
- data/sig/secupdater.rbs +4 -0
- data/spec/e2e/secupdater_spec.rb +112 -0
- data/spec/secupdater/cli_spec.rb +109 -0
- data/spec/secupdater/ecosystem/rubygems_spec.rb +184 -0
- data/spec/secupdater/ecosystem_spec.rb +18 -0
- data/spec/secupdater/publisher/gitlab_spec.rb +106 -0
- data/spec/secupdater/publisher_spec.rb +10 -0
- data/spec/secupdater/runner_spec.rb +56 -0
- data/spec/secupdater/update_spec.rb +262 -0
- data/spec/spec_helper.rb +98 -0
- metadata +124 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require "secupdater"
|
|
2
|
+
require "stringio"
|
|
3
|
+
|
|
4
|
+
RSpec.describe Secupdater::CLI::Commands::Run do
|
|
5
|
+
let(:out) { StringIO.new }
|
|
6
|
+
let(:err) { StringIO.new }
|
|
7
|
+
let(:runner) { instance_double(Secupdater::Runner, call: { "rack" => :published }) }
|
|
8
|
+
let(:env) { {} }
|
|
9
|
+
|
|
10
|
+
def run_cli(*arguments)
|
|
11
|
+
Dry::CLI.new(Secupdater::CLI::Commands).call(arguments: ["run", *arguments], out: out, err: err)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def expect_exit(status, &block)
|
|
15
|
+
expect(&block).to raise_error(SystemExit) { expect(it.status).to eq(status) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
before do
|
|
19
|
+
stub_const("ENV", env)
|
|
20
|
+
allow(Secupdater::Runner).to receive(:new).and_return(runner)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe "#call" do
|
|
24
|
+
context "with --ecosystem rubygems and an advisories path" do
|
|
25
|
+
it "builds a Runner for rubygems at log level :warn and calls it with the advisories path" do
|
|
26
|
+
run_cli("--ecosystem=rubygems", "audit.json")
|
|
27
|
+
|
|
28
|
+
expect(Secupdater::Runner).to have_received(:new).with(ecosystem: "rubygems", log_level: :warn, publisher: nil)
|
|
29
|
+
expect(runner).to have_received(:call).with("audit.json")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context "with --verbose" do
|
|
34
|
+
it "builds the Runner with log level :info" do
|
|
35
|
+
run_cli("--ecosystem=rubygems", "--verbose", "audit.json")
|
|
36
|
+
|
|
37
|
+
expect(Secupdater::Runner).to have_received(:new).with(hash_including(log_level: :info))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
context "with --debug" do
|
|
42
|
+
it "builds the Runner with log level :debug, even when --verbose is also given" do
|
|
43
|
+
run_cli("--ecosystem=rubygems", "--verbose", "--debug", "audit.json")
|
|
44
|
+
|
|
45
|
+
expect(Secupdater::Runner).to have_received(:new).with(hash_including(log_level: :debug))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context "with SECUPDATER_GITLAB_TOKEN set" do
|
|
50
|
+
let(:env) { { "SECUPDATER_GITLAB_TOKEN" => "token", "CI_PROJECT_ID" => "123", "CI_DEFAULT_BRANCH" => "main" } }
|
|
51
|
+
|
|
52
|
+
it "publishes to GitLab using the CI project and default branch" do
|
|
53
|
+
run_cli("--ecosystem=rubygems", "audit.json")
|
|
54
|
+
|
|
55
|
+
expect(Secupdater::Runner).to have_received(:new).with(hash_including(
|
|
56
|
+
publisher: an_instance_of(Secupdater::Publisher::Gitlab).and(
|
|
57
|
+
have_attributes(access_token: "token", project_id: "123", target_branch: "main")
|
|
58
|
+
)
|
|
59
|
+
))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
context "with SECUPDATER_GITLAB_TOKEN set but no CI_PROJECT_ID" do
|
|
64
|
+
let(:env) { { "SECUPDATER_GITLAB_TOKEN" => "token", "CI_DEFAULT_BRANCH" => "main" } }
|
|
65
|
+
|
|
66
|
+
it "prints which variable is missing and exits non-zero" do
|
|
67
|
+
expect_exit(1) { run_cli("--ecosystem=rubygems", "audit.json") }
|
|
68
|
+
|
|
69
|
+
expect(err.string).to eq("SECUPDATER_GITLAB_TOKEN is set but CI_PROJECT_ID isn't\n")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
context "with an unknown --ecosystem" do
|
|
74
|
+
it "prints an error naming the bad option and exits non-zero" do
|
|
75
|
+
expect_exit(1) { run_cli("--ecosystem=npm", "audit.json") }
|
|
76
|
+
|
|
77
|
+
expect(err.string).to include("--ecosystem=npm")
|
|
78
|
+
expect(Secupdater::Runner).not_to have_received(:new)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
context "without an advisories argument" do
|
|
83
|
+
it "prints usage and exits non-zero" do
|
|
84
|
+
expect_exit(1) { run_cli("--ecosystem=rubygems") }
|
|
85
|
+
|
|
86
|
+
expect(err.string).to include("ADVISORIES")
|
|
87
|
+
expect(Secupdater::Runner).not_to have_received(:new)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
context "when the advisories file doesn't exist" do
|
|
92
|
+
before { allow(Secupdater::Runner).to receive(:new).and_call_original }
|
|
93
|
+
|
|
94
|
+
it "prints 'Cannot open <path>' and exits non-zero, rather than an Errno::ENOENT backtrace" do
|
|
95
|
+
expect_exit(1) { run_cli("--ecosystem=rubygems", "missing.json") }
|
|
96
|
+
|
|
97
|
+
expect(err.string).to eq("Cannot open missing.json\n")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
context "when a dependency fails to update" do
|
|
102
|
+
let(:runner) { instance_double(Secupdater::Runner, call: { "rack" => :failed, "nokogiri" => :published }) }
|
|
103
|
+
|
|
104
|
+
it "exits non-zero" do
|
|
105
|
+
expect_exit(1) { run_cli("--ecosystem=rubygems", "audit.json") }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
require "secupdater"
|
|
2
|
+
require "stringio"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Secupdater::Ecosystem::Rubygems do
|
|
6
|
+
subject(:rubygems) { described_class.new }
|
|
7
|
+
|
|
8
|
+
def lockfile(css_parser_version)
|
|
9
|
+
<<~LOCKFILE
|
|
10
|
+
GEM
|
|
11
|
+
remote: https://rubygems.org/
|
|
12
|
+
specs:
|
|
13
|
+
addressable (2.8.0)
|
|
14
|
+
css_parser (#{css_parser_version})
|
|
15
|
+
addressable
|
|
16
|
+
|
|
17
|
+
PLATFORMS
|
|
18
|
+
ruby
|
|
19
|
+
|
|
20
|
+
DEPENDENCIES
|
|
21
|
+
css_parser
|
|
22
|
+
|
|
23
|
+
BUNDLED WITH
|
|
24
|
+
2.5.0
|
|
25
|
+
LOCKFILE
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def spec(name, version) = Secupdater::DependencySpec.new(name, "ruby", Gem::Version.new(version))
|
|
29
|
+
|
|
30
|
+
around do |example|
|
|
31
|
+
Dir.mktmpdir do |dir|
|
|
32
|
+
Dir.chdir(dir) do
|
|
33
|
+
File.write("Gemfile.lock", lockfile("1.0.0"))
|
|
34
|
+
example.run
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe "#parse_advisories(io)" do
|
|
40
|
+
context "bundle-audit JSON with unpatched_gem and insecure_source results" do
|
|
41
|
+
let(:report) do
|
|
42
|
+
{
|
|
43
|
+
results: [
|
|
44
|
+
{
|
|
45
|
+
type: "unpatched_gem",
|
|
46
|
+
gem: { name: "css_parser", version: "1.0.0" },
|
|
47
|
+
advisory: { id: "CVE-2024-0001", cve: "2024-0001", osvdb: nil, ghsa: "abcd-efgh", patched_versions: [">= 1.1.0"] }
|
|
48
|
+
},
|
|
49
|
+
{ type: "insecure_source", source: "http://rubygems.org/" }
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "returns one advisory per unpatched gem with a joined cve/ghsa id and patched_versions" do
|
|
55
|
+
expect(rubygems.parse_advisories(StringIO.new(report.to_json))).to eq([
|
|
56
|
+
Secupdater::SecurityAdvisory.new("css_parser", "cve/2024-0001-ghsa/abcd-efgh", [">= 1.1.0"])
|
|
57
|
+
])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context "an advisory with none of cve, osvdb or ghsa" do
|
|
62
|
+
let(:report) do
|
|
63
|
+
{
|
|
64
|
+
results: [
|
|
65
|
+
{
|
|
66
|
+
type: "unpatched_gem",
|
|
67
|
+
gem: { name: "css_parser", version: "1.0.0" },
|
|
68
|
+
advisory: { id: "RUBYSEC-2024-1", cve: nil, osvdb: nil, ghsa: nil, patched_versions: [">= 1.1.0"] }
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "falls back to the advisory id" do
|
|
75
|
+
expect(rubygems.parse_advisories(StringIO.new(report.to_json)).map(&:report_id)).to eq(["RUBYSEC-2024-1"])
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe "#snapshot" do
|
|
81
|
+
context "fixture Gemfile.lock in root" do
|
|
82
|
+
it "maps gem name to DependencySpec with version" do
|
|
83
|
+
expect(rubygems.snapshot).to eq(
|
|
84
|
+
"addressable" => spec("addressable", "2.8.0"),
|
|
85
|
+
"css_parser" => spec("css_parser", "1.0.0")
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe "#diff_snapshot" do
|
|
92
|
+
context "one gem bumped, one added, one removed" do
|
|
93
|
+
it "returns a diff for each with from/to" do
|
|
94
|
+
before = { "css_parser" => spec("css_parser", "1.0.0"), "public_suffix" => spec("public_suffix", "4.0.0") }
|
|
95
|
+
after = { "css_parser" => spec("css_parser", "1.1.0"), "addressable" => spec("addressable", "2.8.0") }
|
|
96
|
+
|
|
97
|
+
expect(rubygems.diff_snapshot(before, after)).to contain_exactly(
|
|
98
|
+
Secupdater::DependencySpecDiff.new(spec("css_parser", "1.0.0"), Gem::Version.new("1.0.0"), Gem::Version.new("1.1.0")),
|
|
99
|
+
Secupdater::DependencySpecDiff.new(spec("public_suffix", "4.0.0"), Gem::Version.new("4.0.0"), nil),
|
|
100
|
+
Secupdater::DependencySpecDiff.new(spec("addressable", "2.8.0"), nil, Gem::Version.new("2.8.0"))
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context "unchanged gems" do
|
|
106
|
+
it "leaves them out of the diff" do
|
|
107
|
+
snapshot = { "css_parser" => spec("css_parser", "1.0.0") }
|
|
108
|
+
|
|
109
|
+
expect(rubygems.diff_snapshot(snapshot, snapshot.dup)).to be_empty
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe "#satisfies_requirement?" do
|
|
115
|
+
it "matches ~>, >= and a version that's too low" do
|
|
116
|
+
expect(rubygems.satisfies_requirement?("1.1.5", "~> 1.1.4")).to be(true)
|
|
117
|
+
expect(rubygems.satisfies_requirement?("1.2.0", "~> 1.1.4")).to be(false)
|
|
118
|
+
expect(rubygems.satisfies_requirement?("2.0.0", ">= 1.1.0")).to be(true)
|
|
119
|
+
expect(rubygems.satisfies_requirement?("1.0.0", ">= 1.1.0")).to be(false)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "matches comma-separated requirements from bundle audit" do
|
|
123
|
+
expect(rubygems.satisfies_requirement?("7.3.5", "~> 7.3, >= 7.3.5")).to be(true)
|
|
124
|
+
expect(rubygems.satisfies_requirement?("7.3.4", "~> 7.3, >= 7.3.5")).to be(false)
|
|
125
|
+
expect(rubygems.satisfies_requirement?("8.0.0", "~> 7.3, >= 7.3.5")).to be(false)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe "#try_update" do
|
|
130
|
+
let(:status) { instance_double(Process::Status, success?: true, exitstatus: 0) }
|
|
131
|
+
|
|
132
|
+
context "command succeeds" do
|
|
133
|
+
before do
|
|
134
|
+
allow(Open3).to receive(:capture2e) do
|
|
135
|
+
File.write("Gemfile.lock", lockfile("1.1.0"))
|
|
136
|
+
["Bundle updated!", status]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "runs a strict, conservative bundle update at the given level" do
|
|
141
|
+
rubygems.try_update("css_parser", level: :minor)
|
|
142
|
+
|
|
143
|
+
expect(Open3).to have_received(:capture2e).with("bundle", "update", "css_parser", "--conservative", "--minor", "--strict")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "returns the lockfile change with its new and original content" do
|
|
147
|
+
expect(rubygems.try_update("css_parser", level: :patch)).to eq([
|
|
148
|
+
Secupdater::FileChange.new("Gemfile.lock", "update", lockfile("1.1.0"), lockfile("1.0.0"))
|
|
149
|
+
])
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
context "command succeeds without changing anything" do
|
|
154
|
+
before { allow(Open3).to receive(:capture2e).and_return(["Bundle updated!", status]) }
|
|
155
|
+
|
|
156
|
+
it "returns no file changes" do
|
|
157
|
+
expect(rubygems.try_update("css_parser", level: :patch)).to eq([])
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
context "gem name containing shell metacharacters" do
|
|
162
|
+
before { allow(Open3).to receive(:capture2e).and_return(["", status]) }
|
|
163
|
+
|
|
164
|
+
it "passes it as a single argument without going through a shell" do
|
|
165
|
+
rubygems.try_update("css_parser; touch pwned", level: :patch)
|
|
166
|
+
|
|
167
|
+
expect(Open3).to have_received(:capture2e).with("bundle", "update", "css_parser; touch pwned", "--conservative", "--patch", "--strict")
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
context "command exits 1" do
|
|
172
|
+
let(:status) { instance_double(Process::Status, success?: false, exitstatus: 1) }
|
|
173
|
+
|
|
174
|
+
before { allow(Open3).to receive(:capture2e).and_return(["Could not find gem 'css_parser'", status]) }
|
|
175
|
+
|
|
176
|
+
it "raises UpdateCommandFailed with the integer exit code and combined output" do
|
|
177
|
+
expect { rubygems.try_update("css_parser", level: :patch) }.to raise_error(Secupdater::UpdateCommandFailed) do
|
|
178
|
+
expect(it.exit_code).to eq(1)
|
|
179
|
+
expect(it.output).to eq("Could not find gem 'css_parser'")
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "secupdater"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Secupdater::Ecosystem do
|
|
4
|
+
describe ".resolve" do
|
|
5
|
+
context "'rubygems'" do
|
|
6
|
+
it "returns a Secupdater::Ecosystem::Rubygems instance" do
|
|
7
|
+
expect(described_class.resolve("rubygems")).to be_a(Secupdater::Ecosystem::Rubygems)
|
|
8
|
+
expect(described_class.resolve("RubyGems")).to be_a(Secupdater::Ecosystem::Rubygems)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
context "unknown ecosystem" do
|
|
13
|
+
it "raises Secupdater::Error naming the ecosystem" do
|
|
14
|
+
expect { described_class.resolve("npm") }.to raise_error(Secupdater::Error, "Unknown ecosystem: npm")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require "secupdater"
|
|
2
|
+
require "secupdater/publisher"
|
|
3
|
+
require "secupdater/publisher/gitlab"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Secupdater::Publisher::Gitlab do
|
|
6
|
+
subject(:gitlab) { described_class.new("access_token", "123", "main") }
|
|
7
|
+
|
|
8
|
+
let(:ecosystem) { Secupdater::Ecosystem::Rubygems.new }
|
|
9
|
+
let(:dep) { Secupdater::DependencySpec.new("css_parser", "ruby", Gem::Version.new("1.0.0")) }
|
|
10
|
+
let(:advisories) { [Secupdater::SecurityAdvisory.new("css_parser", "cve/2024-0001", [">= 1.1.0"])] }
|
|
11
|
+
let(:bump) { Secupdater::DependencySpecDiff.new(dep, "1.0.0", "1.1.0") }
|
|
12
|
+
let(:changes) { [Secupdater::FileChange.new("Gemfile.lock", "update", "abc", "xyz")] }
|
|
13
|
+
|
|
14
|
+
let(:headers) do
|
|
15
|
+
{
|
|
16
|
+
"PRIVATE-TOKEN" => "access_token",
|
|
17
|
+
content_type: "application/json"
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
let(:existing_merge_requests) { [] }
|
|
22
|
+
|
|
23
|
+
let!(:find_mr_stub) do
|
|
24
|
+
stub_request(:get, "https://gitlab.com/api/v4/projects/123/merge_requests")
|
|
25
|
+
.with(query: { source_branch: "security/css_parser-1.1.0", state: "opened" }, headers: headers)
|
|
26
|
+
.to_return(status: 200, body: existing_merge_requests.to_json)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
let!(:create_commit_stub) do
|
|
30
|
+
stub_request(:post, "https://gitlab.com/api/v4/projects/123/repository/commits")
|
|
31
|
+
.with(
|
|
32
|
+
body: {
|
|
33
|
+
branch: "security/css_parser-1.1.0",
|
|
34
|
+
start_branch: "main",
|
|
35
|
+
commit_message: "Upgrade RubyGems dependency css_parser to version 1.1.0",
|
|
36
|
+
actions: [{ action: "update", file_path: "Gemfile.lock", content: "abc" }]
|
|
37
|
+
},
|
|
38
|
+
headers: headers
|
|
39
|
+
)
|
|
40
|
+
.to_return(status: 201, body: "{}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
let!(:create_mr_stub) do
|
|
44
|
+
stub_request(:post, "https://gitlab.com/api/v4/projects/123/merge_requests")
|
|
45
|
+
.with(body: {
|
|
46
|
+
source_branch: "security/css_parser-1.1.0",
|
|
47
|
+
target_branch: "main",
|
|
48
|
+
remove_source_branch: true,
|
|
49
|
+
labels: [],
|
|
50
|
+
title: "Upgrade RubyGems dependency css_parser to version 1.1.0",
|
|
51
|
+
description: "Upgrade RubyGems dependency css_parser to version 1.1.0 to address the following security advisories:\n\n* cve/2024-0001\n",
|
|
52
|
+
}, headers: headers)
|
|
53
|
+
.to_return(status: 201, body: "{}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "#call(ecosystem, bump, advisories, changes)" do
|
|
57
|
+
it "creates a commit and a merge request" do
|
|
58
|
+
gitlab.call(ecosystem, bump, advisories, changes)
|
|
59
|
+
|
|
60
|
+
expect(create_commit_stub).to have_been_requested
|
|
61
|
+
expect(create_mr_stub).to have_been_requested
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context "deleting a file" do
|
|
65
|
+
let(:changes) { [Secupdater::FileChange.new("Gemfile.lock", "delete", nil, "xyz")] }
|
|
66
|
+
|
|
67
|
+
let!(:create_commit_stub) do
|
|
68
|
+
stub_request(:post, "https://gitlab.com/api/v4/projects/123/repository/commits")
|
|
69
|
+
.with(body: hash_including(actions: [{ action: "delete", file_path: "Gemfile.lock" }]))
|
|
70
|
+
.to_return(status: 201, body: "{}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "doesn't send content for the action" do
|
|
74
|
+
gitlab.call(ecosystem, bump, advisories, changes)
|
|
75
|
+
|
|
76
|
+
expect(create_commit_stub).to have_been_requested
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
context "API returns 4xx" do
|
|
81
|
+
let!(:create_commit_stub) do
|
|
82
|
+
stub_request(:post, "https://gitlab.com/api/v4/projects/123/repository/commits")
|
|
83
|
+
.to_return(status: 400, body: '{"message":"A branch called security/css_parser-1.1.0 already exists"}')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "raises Secupdater::Error including the response body" do
|
|
87
|
+
expect { gitlab.call(ecosystem, bump, advisories, changes) }
|
|
88
|
+
.to raise_error(Secupdater::Error, /400.*already exists/)
|
|
89
|
+
|
|
90
|
+
expect(create_mr_stub).not_to have_been_requested
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
context "an MR for the branch already exists" do
|
|
95
|
+
let(:existing_merge_requests) { [{ iid: 1 }] }
|
|
96
|
+
|
|
97
|
+
it "doesn't create another one" do
|
|
98
|
+
gitlab.call(ecosystem, bump, advisories, changes)
|
|
99
|
+
|
|
100
|
+
expect(find_mr_stub).to have_been_requested
|
|
101
|
+
expect(create_commit_stub).not_to have_been_requested
|
|
102
|
+
expect(create_mr_stub).not_to have_been_requested
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "secupdater"
|
|
2
|
+
require "tmpdir"
|
|
3
|
+
|
|
4
|
+
RSpec.describe Secupdater::Runner do
|
|
5
|
+
describe "#initialize" do
|
|
6
|
+
context "with ecosystem 'rubygems'" do
|
|
7
|
+
it "resolves the Rubygems ecosystem" do
|
|
8
|
+
expect(Secupdater::Ecosystem).to receive(:resolve).with("rubygems").and_call_original
|
|
9
|
+
|
|
10
|
+
described_class.new(ecosystem: "rubygems")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
context "with an unknown ecosystem" do
|
|
15
|
+
it "raises Secupdater::Error naming the ecosystem" do
|
|
16
|
+
expect { described_class.new(ecosystem: "npm") }.to raise_error(Secupdater::Error, /npm/)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe "#call(advisories_path)" do
|
|
22
|
+
subject(:runner) { described_class.new(ecosystem: "rubygems", publisher: publisher) }
|
|
23
|
+
|
|
24
|
+
let(:ecosystem) { instance_double(Secupdater::Ecosystem::Rubygems) }
|
|
25
|
+
let(:publisher) { instance_double(Secupdater::Publisher) }
|
|
26
|
+
let(:update) { instance_double(Secupdater::Update, call: { "rack" => :published }) }
|
|
27
|
+
let(:advisories) { [Secupdater::SecurityAdvisory.new("rack", "cve/1", [">= 2.0"])] }
|
|
28
|
+
|
|
29
|
+
around do |example|
|
|
30
|
+
Dir.mktmpdir do |dir|
|
|
31
|
+
Dir.chdir(dir) { example.run }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
before do
|
|
36
|
+
File.write("audit.json", "{}")
|
|
37
|
+
allow(Secupdater::Ecosystem).to receive(:resolve).and_return(ecosystem)
|
|
38
|
+
allow(ecosystem).to receive(:parse_advisories) { |io| advisories if io.read == "{}" }
|
|
39
|
+
allow(Secupdater::Update).to receive(:new).and_return(update)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "parses the advisories file with the ecosystem and passes them to Update" do
|
|
43
|
+
expect(runner.call("audit.json")).to eq("rack" => :published)
|
|
44
|
+
|
|
45
|
+
expect(Secupdater::Update).to have_received(:new)
|
|
46
|
+
.with(an_instance_of(Dry::Logger::Dispatcher), ecosystem, publisher: publisher)
|
|
47
|
+
expect(update).to have_received(:call).with(advisories)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
context "when the advisories file doesn't exist" do
|
|
51
|
+
it "raises Secupdater::Error naming the path" do
|
|
52
|
+
expect { runner.call("missing.json") }.to raise_error(Secupdater::Error, "Cannot open missing.json")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|