codeclimate-services 1.7.0 → 1.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 08767ab67555c51f2f492eea001853158febb41b
4
- data.tar.gz: f097a06a711a69a01014dc8775bc99f2176bd028
3
+ metadata.gz: 77816c3d07a72abbd79d831e8852592b4b30b654
4
+ data.tar.gz: f8696a4c43abd9b325dc9d8baadf5033c4c8246f
5
5
  SHA512:
6
- metadata.gz: a668a902b808e680c2a0a350506313dcd87b12cbce904d634197c5a109be59a6f6009801dafd841d7df3d69fe7aa6bd2dd7a00c75d8cb067bdebf77c332340a6
7
- data.tar.gz: fe1911f20809ac655ec9b94d8cb35394173a9592bd865d683c4c39ffa63aef3c9aa2867d7e8480bdf99299e00e168d204cb418e4bf1e95f064142f7257bffd9e
6
+ metadata.gz: d6ae01e70ac2bdc798333a52500b11778b47c256c444c7cceb76316c348c532fca4bd405864d4ba12310aac91f81d1a3ae4498a3a986ceb2ef4e0959f888f1e0
7
+ data.tar.gz: 1237a7918f78505f03e166fdd34f0efe21596dc39837d41a25abe1d8b0867e5a1531205f757cf179b20efba0153f7144064604eccbff01252d1a6118b6885ff3
data/.codeclimate.yml ADDED
@@ -0,0 +1,22 @@
1
+ ---
2
+ engines:
3
+ duplication:
4
+ enabled: true
5
+ config:
6
+ languages:
7
+ - ruby
8
+ fixme:
9
+ enabled: true
10
+ rubocop:
11
+ enabled: true
12
+ exclude_fingerprints:
13
+ # Using #=== intentionally in SafeWebhook
14
+ - d1afe90be49c43e85a76bfa58f637804
15
+ # High complexity in http method due to SafeWebhook check
16
+ - f05cea2d219c0f8119eb826067a18eda
17
+ ratings:
18
+ paths:
19
+ - "**.rb"
20
+ exclude_paths:
21
+ - config/
22
+ - spec/
data/Rakefile CHANGED
@@ -1,8 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
3
 
4
- RSpec::Core::RakeTask.new(:spec) do |task|
5
- task.ruby_opts = "-W"
6
- end
4
+ RSpec::Core::RakeTask.new(:spec)
7
5
 
8
6
  task default: :spec
@@ -1,5 +1,6 @@
1
1
  require "active_support/concern"
2
2
  require "cc/service/response_check"
3
+ require "cc/service/safe_webhook"
3
4
 
4
5
  module CC::Service::HTTP
5
6
  extend ActiveSupport::Concern
@@ -38,10 +39,8 @@ module CC::Service::HTTP
38
39
  end
39
40
 
40
41
  def raw_get(url = nil, params = nil, headers = nil)
41
- http.get do |req|
42
- req.url(url) if url
42
+ http_method(:get, url, nil, headers) do |req|
43
43
  req.params.update(params) if params
44
- req.headers.update(headers) if headers
45
44
  yield req if block_given?
46
45
  end
47
46
  end
@@ -59,6 +58,11 @@ module CC::Service::HTTP
59
58
  req.headers.update(headers) if headers
60
59
  req.body = body if body
61
60
  block.call req if block
61
+
62
+ unless allow_internal_webhooks?
63
+ safe_webhook = CC::Service::SafeWebhook.new(url)
64
+ safe_webhook.validate!(req)
65
+ end
62
66
  end
63
67
  end
64
68
 
@@ -99,4 +103,9 @@ module CC::Service::HTTP
99
103
  message: "Success",
100
104
  }
101
105
  end
106
+
107
+ def allow_internal_webhooks?
108
+ var = ENV["CODECLIMATE_ALLOW_INTERNAL_WEBHOOKS"] || ""
109
+ var == "1" || var == "true"
110
+ end
102
111
  end
@@ -0,0 +1,71 @@
1
+ require "ipaddr"
2
+ require "resolv"
3
+
4
+ module CC
5
+ class Service
6
+ class SafeWebhook
7
+ InvalidWebhookURL = Class.new(StandardError)
8
+
9
+ # https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
10
+ # https://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses
11
+ PRIVATE_ADDRESS_SUBNETS = [
12
+ IPAddr.new("10.0.0.0/8"),
13
+ IPAddr.new("172.16.0.0/12"),
14
+ IPAddr.new("192.168.0.0/16"),
15
+ IPAddr.new("fd00::/8"),
16
+ IPAddr.new("127.0.0.1"),
17
+ IPAddr.new("0:0:0:0:0:0:0:1"),
18
+ ].freeze
19
+
20
+ def self.getaddress(host)
21
+ @resolv ||= Resolv::DNS.new
22
+ @resolv.getaddress(host).to_s
23
+ end
24
+
25
+ def initialize(url)
26
+ @url = url
27
+ end
28
+
29
+ # Resolve the Host to an IP address, validate that it doesn't point to
30
+ # anything internal, then alter the request to be for the IP directly with
31
+ # an explicit Host header given.
32
+ #
33
+ # See http://blog.fanout.io/2014/01/27/how-to-safely-invoke-webhooks/#ip-address-blacklisting
34
+ def validate!(request)
35
+ uri = URI.parse(url)
36
+ address = self.class.getaddress(uri.host)
37
+
38
+ if internal?(address)
39
+ raise_invalid("resolves to a private IP address")
40
+ end
41
+
42
+ alter_request(request, uri, address)
43
+ rescue URI::InvalidURIError, Resolv::ResolvError, Resolv::ResolvTimeout => ex
44
+ raise_invalid(ex.message)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :url
50
+
51
+ def internal?(address)
52
+ ip_addr = IPAddr.new(address)
53
+
54
+ PRIVATE_ADDRESS_SUBNETS.any? do |subnet|
55
+ subnet === ip_addr
56
+ end
57
+ end
58
+
59
+ def alter_request(request, uri, address)
60
+ address_uri = uri.dup
61
+ address_uri.host = address
62
+ request.url(address_uri.to_s)
63
+ request.headers.update(Host: uri.host)
64
+ end
65
+
66
+ def raise_invalid(message)
67
+ raise InvalidWebhookURL, "The Webhook URL #{url} is invalid: #{message}"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,5 +1,5 @@
1
1
  module CC
2
2
  module Services
3
- VERSION = "1.7.0".freeze
3
+ VERSION = "1.8.0".freeze
4
4
  end
5
5
  end
@@ -67,8 +67,10 @@ describe CC::Service::GitHubIssues, type: :service do
67
67
  end
68
68
 
69
69
  it "different base url" do
70
+ stub_resolv("example.com", "1.1.1.2")
71
+
70
72
  http_stubs.post request_url do |env|
71
- expect(env[:url].to_s).to eq("http://example.com/#{request_url}")
73
+ expect(env[:url].to_s).to eq("http://1.1.1.2/#{request_url}")
72
74
  [200, {}, '{"number": 2, "html_url": "http://foo.bar"}']
73
75
  end
74
76
 
@@ -107,8 +107,10 @@ describe CC::Service::GitHubPullRequests, type: :service do
107
107
  end
108
108
 
109
109
  it "different base url" do
110
+ stub_resolv("example.com", "1.1.1.2")
111
+
110
112
  http_stubs.post("/repos/pbrisbin/foo/statuses/#{"0" * 40}") do |env|
111
- expect(env[:url].to_s).to eq("http://example.com/repos/pbrisbin/foo/statuses/#{"0" * 40}")
113
+ expect(env[:url].to_s).to eq("http://1.1.1.2/repos/pbrisbin/foo/statuses/#{"0" * 40}")
112
114
  [422, { "x-oauth-scopes" => "gist, user, repo" }, ""]
113
115
  end
114
116
 
@@ -134,14 +134,44 @@ describe CC::Service::GitlabMergeRequests, type: :service do
134
134
  end
135
135
 
136
136
  it "different base url" do
137
+ stub_resolv("gitlab.hal.org", "1.1.1.2")
138
+
137
139
  http_stubs.post("api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}") do |env|
138
- expect(env[:url].to_s).to eq("https://gitlab.hal.org/api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}")
140
+ expect(env[:url].to_s).to eq("https://1.1.1.2/api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}")
139
141
  [404, {}, ""]
140
142
  end
141
143
 
142
144
  expect(receive_test({ base_url: "https://gitlab.hal.org" }, git_url: "ssh://git@gitlab.com/hal/hal9000.git")[:ok]).to eq(true)
143
145
  end
144
146
 
147
+ context "SafeWebhook" do
148
+ it "rewrites the request to be for the resolved IP" do
149
+ stub_resolv("my.gitlab.com", "1.1.1.2")
150
+
151
+ http_stubs.post("api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}") do |env|
152
+ expect(env[:url].to_s).to eq("https://1.1.1.2/api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}")
153
+ expect(env[:request_headers][:Host]).to eq("my.gitlab.com")
154
+ [404, {}, ""]
155
+ end
156
+
157
+ expect(receive_test({ base_url: "https://my.gitlab.com" }, git_url: "ssh://git@gitlab.com/hal/hal9000.git")[:ok]).to eq(true)
158
+ end
159
+
160
+ it "validates that the host doesn't resolve to something internal" do
161
+ stub_resolv("my.gitlab.com", "127.0.0.1")
162
+
163
+ expect do
164
+ receive_test({ base_url: "https://my.gitlab.com" }, git_url: "")
165
+ end.to raise_error(CC::Service::SafeWebhook::InvalidWebhookURL)
166
+
167
+ stub_resolv("my.gitlab.com", "10.0.0.9")
168
+
169
+ expect do
170
+ receive_test({ base_url: "https://my.gitlab.com" }, git_url: "")
171
+ end.to raise_error(CC::Service::SafeWebhook::InvalidWebhookURL)
172
+ end
173
+ end
174
+
145
175
  private
146
176
 
147
177
  def expect_status_update(repo, commit_sha, params)
@@ -0,0 +1,61 @@
1
+ require "spec_helper"
2
+
3
+ class CC::Service
4
+ describe SafeWebhook do
5
+ describe ".getaddress" do
6
+ it "resolves the dns name to a string" do
7
+ address = SafeWebhook.getaddress("codeclimate.com")
8
+
9
+ expect(address).to be_present
10
+ expect(address).to respond_to(:start_with?)
11
+ end
12
+ end
13
+
14
+ describe "#validate!" do
15
+ context "valid webhook URLs" do
16
+ it "rewrites the request to be safe" do
17
+ stub_resolv("example.com", "2.2.2.2")
18
+
19
+ request = double(headers: double)
20
+ expect(request).to receive(:url).with("https://2.2.2.2:3000/foo")
21
+ expect(request.headers).to receive(:update).with(Host: "example.com")
22
+
23
+ safe_webhook = SafeWebhook.new("https://example.com:3000/foo")
24
+ safe_webhook.validate!(request)
25
+ end
26
+ end
27
+
28
+ context "invalid Webhook URLs" do
29
+ it "raises for invalid URL" do
30
+ allow(URI).to receive(:parse).and_raise(URI::InvalidURIError)
31
+
32
+ expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
33
+ end
34
+
35
+ it "raises for un-resolvable URL" do
36
+ allow(SafeWebhook).to receive(:getaddress).and_raise(Resolv::ResolvError)
37
+
38
+ expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
39
+ end
40
+
41
+ it "raises for localhost URLs" do
42
+ stub_resolv("example.com", "127.0.0.1")
43
+
44
+ expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
45
+ end
46
+
47
+ it "raises for internal URLs" do
48
+ stub_resolv("example.com", "10.0.0.1")
49
+
50
+ expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
51
+ end
52
+
53
+ def validate(url)
54
+ request = double
55
+ safe_webhook = SafeWebhook.new(url)
56
+ safe_webhook.validate!(request)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
data/spec/spec_helper.rb CHANGED
@@ -34,4 +34,8 @@ RSpec.configure do |config|
34
34
  # This setting enables warnings. It's recommended, but in some cases may
35
35
  # be too noisy due to issues in dependencies.
36
36
  config.warnings = true
37
+
38
+ config.before(:each) do
39
+ stub_resolv(anything, "1.1.1.1")
40
+ end
37
41
  end
@@ -0,0 +1,10 @@
1
+ module ResolvHelper
2
+ def stub_resolv(host, address)
3
+ allow(CC::Service::SafeWebhook).to receive(:getaddress).
4
+ with(host).and_return(address)
5
+ end
6
+ end
7
+
8
+ RSpec.configure do |conf|
9
+ conf.include ResolvHelper
10
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: codeclimate-services
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Helmkamp
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-15 00:00:00.000000000 Z
11
+ date: 2016-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -143,6 +143,7 @@ executables: []
143
143
  extensions: []
144
144
  extra_rdoc_files: []
145
145
  files:
146
+ - ".codeclimate.yml"
146
147
  - ".gitignore"
147
148
  - ".rspec"
148
149
  - ".rubocop.yml"
@@ -183,6 +184,7 @@ files:
183
184
  - lib/cc/service/invocation/with_retries.rb
184
185
  - lib/cc/service/invocation/with_return_values.rb
185
186
  - lib/cc/service/response_check.rb
187
+ - lib/cc/service/safe_webhook.rb
186
188
  - lib/cc/services.rb
187
189
  - lib/cc/services/asana.rb
188
190
  - lib/cc/services/campfire.rb
@@ -216,12 +218,14 @@ files:
216
218
  - spec/cc/service/jira_spec.rb
217
219
  - spec/cc/service/lighthouse_spec.rb
218
220
  - spec/cc/service/pivotal_tracker_spec.rb
221
+ - spec/cc/service/safe_webhook_spec.rb
219
222
  - spec/cc/service/slack_spec.rb
220
223
  - spec/cc/service/stash_pull_requests_spec.rb
221
224
  - spec/cc/service_spec.rb
222
225
  - spec/fixtures.rb
223
226
  - spec/spec_helper.rb
224
227
  - spec/support/fake_logger.rb
228
+ - spec/support/resolv_helper.rb
225
229
  - spec/support/service_context.rb
226
230
  homepage: ''
227
231
  licenses:
@@ -243,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
243
247
  version: '0'
244
248
  requirements: []
245
249
  rubyforge_project:
246
- rubygems_version: 2.5.1
250
+ rubygems_version: 2.4.5
247
251
  signing_key:
248
252
  specification_version: 4
249
253
  summary: Service classes for Code Climate
@@ -265,10 +269,12 @@ test_files:
265
269
  - spec/cc/service/jira_spec.rb
266
270
  - spec/cc/service/lighthouse_spec.rb
267
271
  - spec/cc/service/pivotal_tracker_spec.rb
272
+ - spec/cc/service/safe_webhook_spec.rb
268
273
  - spec/cc/service/slack_spec.rb
269
274
  - spec/cc/service/stash_pull_requests_spec.rb
270
275
  - spec/cc/service_spec.rb
271
276
  - spec/fixtures.rb
272
277
  - spec/spec_helper.rb
273
278
  - spec/support/fake_logger.rb
279
+ - spec/support/resolv_helper.rb
274
280
  - spec/support/service_context.rb