supergood 0.1.5 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.vscode/settings.json +1 -1
- data/Gemfile.lock +39 -33
- data/lib/supergood/api.rb +20 -11
- data/lib/supergood/client.rb +54 -31
- data/lib/supergood/constants.rb +5 -3
- data/lib/supergood/logger.rb +3 -1
- data/lib/supergood/utils.rb +251 -46
- data/lib/supergood/vendors/http.rb +1 -1
- data/lib/supergood/vendors/net-http.rb +2 -2
- data/lib/supergood/version.rb +1 -1
- data/supergood.gemspec +13 -4
- metadata +184 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc2f971650d5a6065d80fbecdea2f80724f332797f246a4c81dbc8e07bfd1c1b
|
4
|
+
data.tar.gz: fc66b646323cf6dab01694e383038e009eb2667d88eec12056ce50908d10042a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95aed28c6df8865a06edd94999b556a8b2956187b7e78503496102e4d512174ba7164dd2cb587ef37246905dabdb8b44ed672393829e8bf33b4a7d8cb7f47bdb
|
7
|
+
data.tar.gz: f69d675f69015838077bb51d215ad9806e82c6ad12c33f4bcc36dd0aac58098e28120ac29addbcbb981cb0de8a31b49675abdb7a0a4e7dce28c74d30ebfa490d
|
data/.vscode/settings.json
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,31 +1,39 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
supergood (0.1
|
5
|
-
|
4
|
+
supergood (1.0.1)
|
5
|
+
base64 (~> 0.1)
|
6
|
+
digest (~> 3.1, >= 3.1.1)
|
7
|
+
dotenv (~> 2.8, >= 2.8.1)
|
8
|
+
json (~> 2.6, >= 2.6.3)
|
9
|
+
logger (~> 1.5, >= 1.6)
|
10
|
+
rudash (~> 4.1, >= 4.1.2)
|
11
|
+
securerandom (~> 0.2, >= 0.2.2)
|
12
|
+
stringio (~> 3.0)
|
13
|
+
uri (~> 0.12)
|
14
|
+
zlib (~> 3.0)
|
6
15
|
|
7
16
|
GEM
|
8
17
|
remote: https://rubygems.org/
|
9
18
|
specs:
|
10
|
-
addressable (2.8.
|
19
|
+
addressable (2.8.6)
|
11
20
|
public_suffix (>= 2.0.2, < 6.0)
|
12
|
-
base64 (0.
|
21
|
+
base64 (0.2.0)
|
13
22
|
crack (0.4.5)
|
14
23
|
rexml
|
15
24
|
diff-lcs (1.5.0)
|
16
25
|
digest (3.1.1)
|
17
|
-
domain_name (0.
|
18
|
-
unf (>= 0.0.5, < 1.0.0)
|
26
|
+
domain_name (0.6.20240107)
|
19
27
|
dotenv (2.8.1)
|
20
|
-
faraday (2.
|
21
|
-
faraday-net_http (>= 2.0, < 3.
|
22
|
-
|
23
|
-
|
24
|
-
ffi (1.
|
28
|
+
faraday (2.9.0)
|
29
|
+
faraday-net_http (>= 2.0, < 3.2)
|
30
|
+
faraday-net_http (3.1.0)
|
31
|
+
net-http
|
32
|
+
ffi (1.16.3)
|
25
33
|
ffi-compiler (1.0.1)
|
26
34
|
ffi (>= 1.0.0)
|
27
35
|
rake
|
28
|
-
hashdiff (1.0
|
36
|
+
hashdiff (1.1.0)
|
29
37
|
http (5.1.1)
|
30
38
|
addressable (~> 2.8)
|
31
39
|
http-cookie (~> 1.0)
|
@@ -38,51 +46,49 @@ GEM
|
|
38
46
|
httparty (0.21.0)
|
39
47
|
mini_mime (>= 1.0.0)
|
40
48
|
multi_xml (>= 0.5.2)
|
41
|
-
json (2.
|
49
|
+
json (2.7.1)
|
42
50
|
llhttp-ffi (0.4.0)
|
43
51
|
ffi-compiler (~> 1.0)
|
44
52
|
rake (~> 13.0)
|
45
|
-
logger (1.
|
46
|
-
mime-types (3.
|
53
|
+
logger (1.6.0)
|
54
|
+
mime-types (3.5.2)
|
47
55
|
mime-types-data (~> 3.2015)
|
48
|
-
mime-types-data (3.2023.
|
49
|
-
mini_mime (1.1.
|
56
|
+
mime-types-data (3.2023.1205)
|
57
|
+
mini_mime (1.1.5)
|
50
58
|
multi_xml (0.6.0)
|
59
|
+
net-http (0.4.1)
|
60
|
+
uri
|
51
61
|
netrc (0.11.0)
|
52
|
-
public_suffix (5.0.
|
53
|
-
rake (13.0
|
62
|
+
public_suffix (5.0.4)
|
63
|
+
rake (13.1.0)
|
54
64
|
rest-client (2.1.0)
|
55
65
|
http-accept (>= 1.7.0, < 2.0)
|
56
66
|
http-cookie (>= 1.0.2, < 2.0)
|
57
67
|
mime-types (>= 1.16, < 4.0)
|
58
68
|
netrc (~> 0.8)
|
59
|
-
rexml (3.2.
|
69
|
+
rexml (3.2.6)
|
60
70
|
rspec (3.12.0)
|
61
71
|
rspec-core (~> 3.12.0)
|
62
72
|
rspec-expectations (~> 3.12.0)
|
63
73
|
rspec-mocks (~> 3.12.0)
|
64
|
-
rspec-core (3.12.
|
74
|
+
rspec-core (3.12.2)
|
65
75
|
rspec-support (~> 3.12.0)
|
66
|
-
rspec-expectations (3.12.
|
76
|
+
rspec-expectations (3.12.3)
|
67
77
|
diff-lcs (>= 1.2.0, < 2.0)
|
68
78
|
rspec-support (~> 3.12.0)
|
69
|
-
rspec-mocks (3.12.
|
79
|
+
rspec-mocks (3.12.6)
|
70
80
|
diff-lcs (>= 1.2.0, < 2.0)
|
71
81
|
rspec-support (~> 3.12.0)
|
72
|
-
rspec-support (3.12.
|
73
|
-
ruby2_keywords (0.0.5)
|
82
|
+
rspec-support (3.12.1)
|
74
83
|
rudash (4.1.2)
|
75
|
-
securerandom (0.
|
76
|
-
stringio (3.0
|
77
|
-
|
78
|
-
|
79
|
-
unf_ext (0.0.8.2)
|
80
|
-
uri (0.12.0)
|
81
|
-
webmock (3.18.1)
|
84
|
+
securerandom (0.3.1)
|
85
|
+
stringio (3.1.0)
|
86
|
+
uri (0.13.0)
|
87
|
+
webmock (3.19.1)
|
82
88
|
addressable (>= 2.8.0)
|
83
89
|
crack (>= 0.3.2)
|
84
90
|
hashdiff (>= 0.4.0, < 2.0.0)
|
85
|
-
zlib (3.
|
91
|
+
zlib (3.1.0)
|
86
92
|
|
87
93
|
PLATFORMS
|
88
94
|
ruby
|
data/lib/supergood/api.rb
CHANGED
@@ -8,7 +8,9 @@ module Supergood
|
|
8
8
|
@base_url = base_url
|
9
9
|
@header_options = {
|
10
10
|
'Content-Type' => 'application/json',
|
11
|
-
'Authorization' =>
|
11
|
+
'Authorization' => "Basic #{Base64.encode64("#{client_id}:#{client_secret}").gsub(/\n/, '')}",
|
12
|
+
'supergood-api' => 'supergood-rb',
|
13
|
+
'supergood-api-version' => VERSION
|
12
14
|
}
|
13
15
|
@local_only = client_id == LOCAL_CLIENT_ID && client_secret == LOCAL_CLIENT_SECRET
|
14
16
|
end
|
@@ -29,11 +31,12 @@ module Supergood
|
|
29
31
|
if @local_only
|
30
32
|
@log.debug(payload)
|
31
33
|
else
|
32
|
-
uri = URI(@base_url
|
34
|
+
uri = URI("#{@base_url}/events")
|
33
35
|
response = Net::HTTP.post(uri, payload.to_json, @header_options)
|
34
|
-
|
35
|
-
|
36
|
-
|
36
|
+
|
37
|
+
return JSON.parse(response.body) if response.code == '200'
|
38
|
+
|
39
|
+
if response.code == '401'
|
37
40
|
raise SupergoodException.new ERRORS[:UNAUTHORIZED]
|
38
41
|
elsif response.code != '200' && response.code != '201'
|
39
42
|
raise SupergoodException.new ERRORS[:POSTING_EVENTS]
|
@@ -45,14 +48,20 @@ module Supergood
|
|
45
48
|
if @local_only
|
46
49
|
@log.debug(payload)
|
47
50
|
else
|
48
|
-
uri = URI(@base_url
|
51
|
+
uri = URI("#{@base_url}/errors")
|
49
52
|
response = Net::HTTP.post(uri, payload.to_json, @header_options)
|
50
|
-
if response.code == '200'
|
51
|
-
|
52
|
-
|
53
|
-
@log.warn(ERRORS[:POSTING_ERRORS])
|
54
|
-
end
|
53
|
+
return JSON.parse(response.body, symbolize_names: true) if response.code == '200'
|
54
|
+
|
55
|
+
@log.warn(ERRORS[:POSTING_ERRORS])
|
55
56
|
end
|
56
57
|
end
|
58
|
+
|
59
|
+
def get_remote_config
|
60
|
+
uri = URI(@base_url + '/config')
|
61
|
+
response = Net::HTTP.get_response(uri, @header_options)
|
62
|
+
return JSON.parse(response.body) if response.code == '200'
|
63
|
+
|
64
|
+
raise SupergoodException.new ERRORS[:CONFIG_FETCH_ERROR]
|
65
|
+
end
|
57
66
|
end
|
58
67
|
end
|
data/lib/supergood/client.rb
CHANGED
@@ -31,7 +31,6 @@ module Supergood
|
|
31
31
|
|
32
32
|
@allowed_domains = @config[:allowedDomains]
|
33
33
|
@ignored_domains = @config[:ignoredDomains]
|
34
|
-
@keys_to_hash = @config[:keysToHash]
|
35
34
|
@logger = Supergood::Logger.new(@api, @config, @api.header_options)
|
36
35
|
|
37
36
|
@api.set_logger(@logger)
|
@@ -40,13 +39,15 @@ module Supergood
|
|
40
39
|
@response_cache = {}
|
41
40
|
|
42
41
|
@interval_thread = set_interval(@config[:flushInterval]) { flush_cache }
|
42
|
+
@remote_config_thread = set_interval(@config[:remoteConfigFetchInterval]) { fetch_and_process_remote_config }
|
43
43
|
|
44
44
|
@http_clients = [
|
45
45
|
Supergood::Vendor::NetHTTP,
|
46
46
|
Supergood::Vendor::HTTPrb
|
47
47
|
]
|
48
48
|
|
49
|
-
|
49
|
+
fetch_and_process_remote_config
|
50
|
+
patch_all
|
50
51
|
self
|
51
52
|
end
|
52
53
|
|
@@ -58,6 +59,15 @@ module Supergood
|
|
58
59
|
@api
|
59
60
|
end
|
60
61
|
|
62
|
+
def fetch_and_process_remote_config
|
63
|
+
begin
|
64
|
+
remote_config = @api.get_remote_config
|
65
|
+
@config = @config.merge({ :remote_config => Supergood::Utils.process_remote_config(remote_config) })
|
66
|
+
rescue => e
|
67
|
+
log.error({}, e, ERRORS[:CONFIG_FETCH_ERROR])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
61
71
|
def flush_cache(force = false)
|
62
72
|
# If there's notthing in the response cache, and we're not forcing a flush, then return
|
63
73
|
|
@@ -67,11 +77,8 @@ module Supergood
|
|
67
77
|
return
|
68
78
|
end
|
69
79
|
|
70
|
-
data = @response_cache.values
|
71
|
-
|
72
|
-
if force
|
73
|
-
data += @request_cache.values
|
74
|
-
end
|
80
|
+
data = Supergood::Utils.prepare_data(@response_cache.values, @config[:remote_config], @config[:forceRedactAll])
|
81
|
+
data += Supergood::Utils.prepare_data(@request_cache.values, @config[:remote_config], @config[:forceRedactAll]) if force
|
75
82
|
|
76
83
|
begin
|
77
84
|
api.post_events(data)
|
@@ -86,6 +93,7 @@ module Supergood
|
|
86
93
|
|
87
94
|
def cleanup()
|
88
95
|
@interval_thread.kill
|
96
|
+
@remote_config_thread.kill
|
89
97
|
unpatch_all()
|
90
98
|
end
|
91
99
|
|
@@ -117,16 +125,25 @@ module Supergood
|
|
117
125
|
end
|
118
126
|
|
119
127
|
def intercept(request)
|
128
|
+
remote_config = @config[:remote_config]
|
129
|
+
|
130
|
+
if remote_config.nil?
|
131
|
+
response = yield
|
132
|
+
return response[:original_response]
|
133
|
+
end
|
134
|
+
|
120
135
|
request_id = SecureRandom.uuid
|
121
136
|
requested_at = Time.now
|
122
|
-
|
123
|
-
|
137
|
+
|
138
|
+
endpoint_config = Supergood::Utils.get_endpoint_config(request.transform_keys(&:to_s), remote_config)
|
139
|
+
ignore_endpoint = endpoint_config ? endpoint_config['ignored'] : false
|
140
|
+
|
141
|
+
if !ignore_endpoint && !ignored?(request[:domain])
|
124
142
|
cache_request(request_id, requested_at, request)
|
125
143
|
end
|
126
144
|
|
127
145
|
response = yield
|
128
|
-
if !ignored?(request[:domain]) && defined?(response)
|
129
|
-
puts "Caching Response"
|
146
|
+
if !ignore_endpoint && !ignored?(request[:domain]) && defined?(response)
|
130
147
|
cache_response(request_id, requested_at, response)
|
131
148
|
end
|
132
149
|
|
@@ -134,41 +151,47 @@ module Supergood
|
|
134
151
|
end
|
135
152
|
|
136
153
|
def cache_request(request_id, requested_at, request)
|
154
|
+
if !@config[:remote_config]
|
155
|
+
return
|
156
|
+
end
|
157
|
+
|
137
158
|
begin
|
138
159
|
request_payload = {
|
139
|
-
id
|
140
|
-
headers
|
141
|
-
method
|
142
|
-
url
|
143
|
-
path
|
144
|
-
search
|
145
|
-
body
|
146
|
-
requestedAt
|
160
|
+
'id' => request_id,
|
161
|
+
'headers' => Supergood::Utils.safe_parse_json(request[:headers]),
|
162
|
+
'method' => request[:method],
|
163
|
+
'url' => request[:url],
|
164
|
+
'path' => request[:path],
|
165
|
+
'search' => request[:search] || '',
|
166
|
+
'body' => Supergood::Utils.safe_parse_json(request[:body]),
|
167
|
+
'requestedAt' => requested_at
|
147
168
|
}
|
148
169
|
@request_cache[request_id] = {
|
149
|
-
request
|
170
|
+
'request' => request_payload
|
150
171
|
}
|
151
172
|
rescue => e
|
152
|
-
log.error({ request
|
173
|
+
log.error({ 'request' => request }, e, ERRORS[:CACHING_REQUEST])
|
153
174
|
end
|
154
175
|
end
|
155
176
|
|
156
177
|
def cache_response(request_id, requested_at, response)
|
178
|
+
if !@config[:remote_config]
|
179
|
+
return
|
180
|
+
end
|
181
|
+
|
157
182
|
begin
|
158
183
|
responded_at = Time.now
|
159
184
|
duration = (responded_at - requested_at) * 1000
|
160
185
|
request_payload = @request_cache[request_id]
|
161
186
|
response_payload = {
|
162
|
-
headers
|
163
|
-
status
|
164
|
-
statusText
|
165
|
-
body
|
166
|
-
respondedAt
|
167
|
-
duration
|
187
|
+
'headers' => Supergood::Utils.safe_parse_json(response[:headers]),
|
188
|
+
'status' => response[:status],
|
189
|
+
'statusText' => response[:statusText],
|
190
|
+
'body' => Supergood::Utils.safe_parse_json(response[:body]),
|
191
|
+
'respondedAt' => responded_at,
|
192
|
+
'duration' => duration.round
|
168
193
|
}
|
169
|
-
@response_cache[request_id] =
|
170
|
-
response: response_payload
|
171
|
-
}), @keys_to_hash)
|
194
|
+
@response_cache[request_id] = request_payload.merge({ 'response' => response_payload })
|
172
195
|
@request_cache.delete(request_id)
|
173
196
|
rescue => e
|
174
197
|
log.error(
|
@@ -179,7 +202,7 @@ module Supergood
|
|
179
202
|
end
|
180
203
|
|
181
204
|
def ignored?(domain)
|
182
|
-
base_domain =
|
205
|
+
base_domain = Supergood::Utils.get_host_without_www(@base_url)
|
183
206
|
if domain == base_domain
|
184
207
|
return true
|
185
208
|
elsif @allowed_domains.any?
|
data/lib/supergood/constants.rb
CHANGED
@@ -5,7 +5,8 @@ ERRORS = {
|
|
5
5
|
POSTING_EVENTS: 'Error Posting Events',
|
6
6
|
POSTING_ERRORS: 'Error Posting Errors',
|
7
7
|
WRITING_TO_DISK: 'Error writing to disk',
|
8
|
-
TEST_ERROR: 'Test Error for Testing
|
8
|
+
TEST_ERROR: 'Test Error for Testing Purposes',
|
9
|
+
CONFIG_FETCH_ERROR: 'Error Fetching Remote Config',
|
9
10
|
UNAUTHORIZED: 'Unauthorized: Invalid Client ID or Secret. Exiting.',
|
10
11
|
NO_CLIENT_ID:
|
11
12
|
'No Client ID Provided, set SUPERGOOD_CLIENT_ID or pass it as an argument',
|
@@ -16,13 +17,14 @@ ERRORS = {
|
|
16
17
|
LOCAL_CLIENT_ID = 'local-client-id';
|
17
18
|
LOCAL_CLIENT_SECRET = 'local-client-secret';
|
18
19
|
|
19
|
-
DEFAULT_SUPERGOOD_BYTE_LIMIT =
|
20
|
+
DEFAULT_SUPERGOOD_BYTE_LIMIT = 500_000
|
20
21
|
|
21
22
|
DEFAULT_CONFIG = {
|
22
|
-
keysToHash: [],
|
23
23
|
flushInterval: 1000,
|
24
|
+
remoteConfigFetchInterval: 10_000,
|
24
25
|
ignoredDomains: [],
|
25
26
|
allowedDomains: [],
|
27
|
+
forceRedactAll: true
|
26
28
|
}
|
27
29
|
|
28
30
|
# GZIP_START_BYTES = b'\x1f\x8b'
|
data/lib/supergood/logger.rb
CHANGED
data/lib/supergood/utils.rb
CHANGED
@@ -1,62 +1,28 @@
|
|
1
1
|
require 'rudash'
|
2
2
|
require 'digest'
|
3
|
+
require 'uri'
|
4
|
+
require 'json'
|
3
5
|
|
4
6
|
module Supergood
|
5
7
|
module Utils
|
6
|
-
def self.hash_value(input)
|
7
|
-
hash = Digest::SHA1.new
|
8
|
-
if input == nil
|
9
|
-
return ''
|
10
|
-
elsif input.class == Array
|
11
|
-
return [Base64.strict_encode64(hash.update(input.to_json).to_s)]
|
12
|
-
elsif input.class == Hash
|
13
|
-
return {'hashed': Base64.strict_encode64(hash.update(input.to_json).to_s)}
|
14
|
-
elsif input.class == String
|
15
|
-
return Base64.strict_encode64(hash.update(input).to_s)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
# Hash values from specified keys, or hash if the bodies exceed a byte limit
|
20
|
-
def self.hash_values_from_keys(obj, keys_to_hash, byte_limit=DEFAULT_SUPERGOOD_BYTE_LIMIT)
|
21
|
-
_obj = obj
|
22
|
-
|
23
|
-
if !keys_to_hash.include?('response.body')
|
24
|
-
payload = R_.get(_obj, 'response.body')
|
25
|
-
payload_size = payload.to_s.length()
|
26
|
-
if(payload_size >= byte_limit)
|
27
|
-
R_.set(_obj, 'response.body', Supergood::Utils.hash_value(payload))
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
if !keys_to_hash.include?('request.body')
|
32
|
-
payload = R_.get(_obj, 'request.body')
|
33
|
-
payload_size = payload.to_s.length()
|
34
|
-
if(payload_size >= byte_limit)
|
35
|
-
R_.set(_obj, 'request.body', Supergood::Utils.hash_value(payload))
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
keys_to_hash.each { |key|
|
40
|
-
value = R_.get(_obj, key)
|
41
|
-
if !!value
|
42
|
-
R_.set(_obj, key, Supergood::Utils.hash_value(value))
|
43
|
-
end
|
44
|
-
}
|
45
8
|
|
46
|
-
|
9
|
+
def self.get_host_without_www(url)
|
10
|
+
uri = URI.parse(url)
|
11
|
+
uri = URI.parse("http://#{url}") if uri.scheme.nil?
|
12
|
+
host = uri.host.downcase
|
13
|
+
host.start_with?('www.') ? host[4..] : host
|
47
14
|
end
|
48
15
|
|
49
16
|
def self.safe_parse_json(input)
|
50
|
-
if !input || input == ''
|
51
|
-
return ''
|
52
|
-
end
|
17
|
+
return '' if !input || input == ''
|
53
18
|
|
54
19
|
begin
|
55
|
-
|
20
|
+
JSON.parse(input)
|
56
21
|
rescue => e
|
57
22
|
input
|
58
23
|
end
|
59
24
|
end
|
25
|
+
|
60
26
|
def self.get_header(request_or_response)
|
61
27
|
header = {}
|
62
28
|
request_or_response.each_header do |k,v|
|
@@ -66,11 +32,250 @@ module Supergood
|
|
66
32
|
end
|
67
33
|
|
68
34
|
def self.request_url(http, request)
|
69
|
-
URI::DEFAULT_PARSER.unescape("http#{
|
35
|
+
URI::DEFAULT_PARSER.unescape("http#{'s' if http.use_ssl?}://#{http.address}#{request.path}")
|
70
36
|
end
|
71
37
|
|
72
38
|
def self.make_config(config)
|
73
|
-
|
39
|
+
DEFAULT_CONFIG.merge(config)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.process_remote_config(remote_config_payload)
|
43
|
+
remote_config_payload ||= []
|
44
|
+
remote_config_payload.reduce({}) do |remote_config, domain_config|
|
45
|
+
domain = domain_config['domain']
|
46
|
+
endpoints = domain_config['endpoints']
|
47
|
+
endpoint_config = endpoints.reduce({}) do |config, endpoint|
|
48
|
+
matching_regex = endpoint['matchingRegex']
|
49
|
+
regex = matching_regex['regex']
|
50
|
+
location = matching_regex['location']
|
51
|
+
|
52
|
+
endpoint_configuration = endpoint['endpointConfiguration']
|
53
|
+
action = endpoint_configuration['action']
|
54
|
+
sensitive_keys = endpoint_configuration['sensitiveKeys'] || []
|
55
|
+
sensitive_keys = sensitive_keys.map { |key| key['keyPath'] }
|
56
|
+
|
57
|
+
config[regex] = {
|
58
|
+
'location' => location,
|
59
|
+
'regex' => regex,
|
60
|
+
'ignored' => action == 'Ignore',
|
61
|
+
'sensitive_keys' => sensitive_keys
|
62
|
+
}
|
63
|
+
|
64
|
+
config
|
65
|
+
end
|
66
|
+
|
67
|
+
remote_config[domain] = endpoint_config
|
68
|
+
remote_config
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.get_str_representation_from_path(request, location)
|
73
|
+
url = URI(request['url'])
|
74
|
+
|
75
|
+
case location
|
76
|
+
when 'domain'
|
77
|
+
get_host_without_www(url)
|
78
|
+
when 'url'
|
79
|
+
url.to_s
|
80
|
+
when 'path'
|
81
|
+
url.path
|
82
|
+
when 'requestHeaders'
|
83
|
+
request['headers'].to_s
|
84
|
+
when 'requestBody'
|
85
|
+
request['body'].to_s
|
86
|
+
else
|
87
|
+
request[location.to_sym].to_s if request.key?(location.to_sym)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.get_endpoint_config(request, remote_config)
|
92
|
+
domain = remote_config.keys.find { |d| get_host_without_www(request['url']).include?(d) }
|
93
|
+
return nil unless domain
|
94
|
+
|
95
|
+
endpoint_configs = remote_config[domain]
|
96
|
+
endpoint_configs.each_value do |endpoint_config|
|
97
|
+
regex = endpoint_config['regex']
|
98
|
+
location = endpoint_config['location']
|
99
|
+
regex_obj = Regexp.new(regex)
|
100
|
+
str_representation = get_str_representation_from_path(request, location)
|
101
|
+
next unless str_representation
|
102
|
+
return endpoint_config if regex_obj.match?(str_representation)
|
103
|
+
end
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.expand(parts, obj, key_path)
|
108
|
+
path = key_path
|
109
|
+
return [path] if parts.empty?
|
110
|
+
|
111
|
+
part = parts.first
|
112
|
+
is_property = !part.start_with?('[')
|
113
|
+
separator = !path.empty? && is_property ? '.' : ''
|
114
|
+
|
115
|
+
# Check for array notations
|
116
|
+
if part.match?(/\[\*?\]/)
|
117
|
+
return [] unless obj.is_a?(Array)
|
118
|
+
|
119
|
+
# Expand for each element in the array
|
120
|
+
obj.flat_map.with_index do |_, index|
|
121
|
+
expand(parts[1..], obj[index], "#{path}#{separator}[#{index}]")
|
122
|
+
end
|
123
|
+
elsif part.start_with?('[') && part.end_with?(']')
|
124
|
+
# Specific index in the array
|
125
|
+
index = part[1...-1].to_i
|
126
|
+
if index.is_a?(Numeric) && index < obj.length
|
127
|
+
expand(parts[1..], obj[index], "#{path}#{separator}#{part}")
|
128
|
+
else
|
129
|
+
[]
|
130
|
+
end
|
131
|
+
else
|
132
|
+
if obj && obj.is_a?(Hash) && obj.key?(part)
|
133
|
+
expand(parts[1..], obj[part], "#{path}#{separator}#{part}")
|
134
|
+
else
|
135
|
+
[]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.expand_key(key, obj)
|
141
|
+
parts = key.scan(/[^.\[\]]+|\[\d*\]|\[\*\]/) || []
|
142
|
+
expand(parts, obj, '')
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.expand_sensitive_key_set_for_arrays(obj, sensitive_keys)
|
146
|
+
sensitive_keys.flat_map { |key| expand_key(key, obj) }
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.marshal_key_path(keypath)
|
150
|
+
keypath.gsub(/^requestHeaders/, 'request.headers')
|
151
|
+
.gsub(/^requestBody/, 'request.body')
|
152
|
+
.gsub(/^responseHeaders/, 'response.headers')
|
153
|
+
.gsub(/^responseBody/, 'response.body')
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.unmarshal_key_path(keypath)
|
157
|
+
keypath.gsub(/^request\.headers/, 'requestHeaders')
|
158
|
+
.gsub(/^request\.body/, 'requestBody')
|
159
|
+
.gsub(/^response\.headers/, 'responseHeaders')
|
160
|
+
.gsub(/^response\.body/, 'responseBody')
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.set_value_to_nil(hash, key_path)
|
164
|
+
keys = key_path.split('.')
|
165
|
+
current_key = keys.first
|
166
|
+
index = current_key.match(/\[(\d+)\]/)
|
167
|
+
|
168
|
+
index = index[1].to_i if index
|
169
|
+
|
170
|
+
# Convert current_key to symbol if necessary
|
171
|
+
current_key = current_key.gsub(/\[\d+\]/, '') if index
|
172
|
+
|
173
|
+
return hash unless hash.keys.include?(current_key)
|
174
|
+
|
175
|
+
if keys.length == 1
|
176
|
+
index ? hash[current_key][index] = nil : hash[current_key] = nil
|
177
|
+
elsif hash[current_key].is_a?(Hash)
|
178
|
+
set_value_to_nil(hash[current_key], keys[1..].join('.'))
|
179
|
+
elsif hash[current_key].is_a?(Array)
|
180
|
+
set_value_to_nil(hash[current_key][index], keys[1..].join('.'))
|
181
|
+
end
|
182
|
+
|
183
|
+
hash
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.find_leaf_key_paths(structure, current_path = [])
|
187
|
+
key_paths = []
|
188
|
+
|
189
|
+
if structure.is_a?(Hash)
|
190
|
+
# Iterate through each key-value pair in the hash
|
191
|
+
structure.each do |key, value|
|
192
|
+
# Recursively find key paths in the value
|
193
|
+
key_paths += find_leaf_key_paths(value, current_path + [key.to_s])
|
194
|
+
end
|
195
|
+
elsif structure.is_a?(Array)
|
196
|
+
# Iterate through each element in the array
|
197
|
+
structure.each_with_index do |element, index|
|
198
|
+
# Modify how indices are appended to the path
|
199
|
+
# Check if the last element in the current_path is a hash key or an array index
|
200
|
+
if current_path.last && current_path.last.include?('[')
|
201
|
+
new_path = current_path[0...-1] + ["#{current_path.last}[#{index}]"]
|
202
|
+
else
|
203
|
+
new_path = current_path + ["[#{index}]"]
|
204
|
+
end
|
205
|
+
|
206
|
+
# Recursively find key paths in the element
|
207
|
+
key_paths += find_leaf_key_paths(element, new_path)
|
208
|
+
end
|
209
|
+
else
|
210
|
+
# Leaf node: construct the key path and add it to the list
|
211
|
+
key_path = current_path.join('.').gsub('.[', '[')
|
212
|
+
key_paths << key_path unless key_path.empty?
|
213
|
+
end
|
214
|
+
|
215
|
+
key_paths
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.redact_values_from_keys(event, remote_config, force_redact_all)
|
219
|
+
sensitive_key_metadata = []
|
220
|
+
endpoint_config = get_endpoint_config(event['request'], remote_config)
|
221
|
+
|
222
|
+
unless (endpoint_config && endpoint_config['sensitive_keys'].any?) || force_redact_all
|
223
|
+
return { 'event' => event, 'sensitive_key_metadata' => sensitive_key_metadata }
|
224
|
+
end
|
225
|
+
|
226
|
+
if force_redact_all
|
227
|
+
# Need response.body in path
|
228
|
+
sensitive_keys = find_leaf_key_paths(event['response']['body'], ['response', 'body'])
|
229
|
+
sensitive_keys += find_leaf_key_paths(event['request']['body'], ['request', 'body'])
|
230
|
+
sensitive_keys += find_leaf_key_paths(event['request']['headers'], ['request', 'headers'])
|
231
|
+
sensitive_keys += find_leaf_key_paths(event['response']['headers'], ['response', 'headers'])
|
232
|
+
else
|
233
|
+
sensitive_keys = endpoint_config['sensitive_keys']
|
234
|
+
end
|
235
|
+
|
236
|
+
sensitive_keys = expand_sensitive_key_set_for_arrays(
|
237
|
+
event, sensitive_keys.map { |key| marshal_key_path(key) }
|
238
|
+
)
|
239
|
+
sensitive_keys.each do |key_path|
|
240
|
+
value = R_.get(event, key_path)
|
241
|
+
event = set_value_to_nil(event, key_path)
|
242
|
+
# Add sensitive key for array expansion
|
243
|
+
sensitive_key_metadata << { 'keyPath' => unmarshal_key_path(key_path) }.merge(redact_value(value))
|
244
|
+
end
|
245
|
+
|
246
|
+
{ 'event' => event, 'sensitive_key_metadata' => sensitive_key_metadata }
|
247
|
+
end
|
248
|
+
|
249
|
+
def self.redact_value(input)
|
250
|
+
data_length = 0
|
251
|
+
data_type = 'null'
|
252
|
+
case input
|
253
|
+
when Array
|
254
|
+
data_length = input.size
|
255
|
+
data_type = 'array'
|
256
|
+
when Hash
|
257
|
+
data_length = input.to_json.bytesize
|
258
|
+
data_type = 'object'
|
259
|
+
when String
|
260
|
+
data_length = input.size
|
261
|
+
data_type = 'string'
|
262
|
+
when Numeric
|
263
|
+
data_length = input.to_s.size
|
264
|
+
data_type = input.integer? ? 'integer' : 'float'
|
265
|
+
when TrueClass, FalseClass # This is a better way to check for booleans
|
266
|
+
data_length = 1
|
267
|
+
data_type = 'boolean'
|
268
|
+
end
|
269
|
+
{ 'length' => data_length, 'type' => data_type }
|
270
|
+
end
|
271
|
+
|
272
|
+
def self.prepare_data(events, remote_config, force_redact_all)
|
273
|
+
events.map do |event|
|
274
|
+
redacted_event_with_metadata = redact_values_from_keys(event, remote_config, force_redact_all)
|
275
|
+
redacted_event_with_metadata['event'].merge(
|
276
|
+
'metadata': { 'sensitiveKeys': redacted_event_with_metadata['sensitive_key_metadata'] }
|
277
|
+
)
|
278
|
+
end
|
74
279
|
end
|
75
280
|
end
|
76
281
|
end
|
@@ -14,7 +14,7 @@ module Supergood
|
|
14
14
|
url: original_request_payload.uri.to_s,
|
15
15
|
path: original_request_payload.uri.path,
|
16
16
|
search: original_request_payload.uri.query,
|
17
|
-
domain: original_request_payload.uri.host
|
17
|
+
domain: Supergood::Utils.get_host_without_www(original_request_payload.uri.host)
|
18
18
|
}
|
19
19
|
Supergood.intercept(request) do
|
20
20
|
original_response = original_perform(original_request_payload, original_options)
|
@@ -10,7 +10,7 @@ module Supergood
|
|
10
10
|
block = lambda do |x|
|
11
11
|
alias original_request_method request
|
12
12
|
def request(original_request_payload, body = nil, &block)
|
13
|
-
http = self
|
13
|
+
http = self
|
14
14
|
url = Supergood::Utils.request_url(http, original_request_payload)
|
15
15
|
uri = URI.parse(url)
|
16
16
|
request = {
|
@@ -20,7 +20,7 @@ module Supergood
|
|
20
20
|
url: url,
|
21
21
|
path: original_request_payload.path,
|
22
22
|
search: uri.query,
|
23
|
-
domain: uri.host
|
23
|
+
domain: Supergood::Utils.get_host_without_www(uri.host)
|
24
24
|
}
|
25
25
|
Supergood.intercept(request) do
|
26
26
|
original_response = original_request_method(original_request_payload, body, &block)
|
data/lib/supergood/version.rb
CHANGED
data/supergood.gemspec
CHANGED
@@ -18,14 +18,23 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.metadata = { 'source_code_uri' => 'https://github.com/supergoodsystems/supergood-rb', 'license' => 'BUSL-1.1' }
|
19
19
|
s.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
20
20
|
|
21
|
-
s.
|
21
|
+
s.add_runtime_dependency 'base64', '~> 0.1'
|
22
|
+
s.add_runtime_dependency 'digest', '~> 3.1', '>= 3.1.1'
|
23
|
+
s.add_runtime_dependency 'dotenv', '~> 2.8', '>= 2.8.1'
|
24
|
+
s.add_runtime_dependency 'json', '~> 2.6', '>= 2.6.3'
|
25
|
+
s.add_runtime_dependency 'logger', '~> 1.5', '>= 1.6'
|
26
|
+
s.add_runtime_dependency 'rudash', '~> 4.1', '>= 4.1.2'
|
27
|
+
s.add_runtime_dependency 'securerandom', '~> 0.2', '>= 0.2.2'
|
28
|
+
s.add_runtime_dependency 'stringio', '~> 3.0'
|
29
|
+
s.add_runtime_dependency 'uri', '~> 0.12'
|
30
|
+
s.add_runtime_dependency 'zlib', '~> 3.0'
|
22
31
|
|
23
|
-
s.add_development_dependency '
|
24
|
-
s.add_development_dependency 'httparty', '~> 0.21.0'
|
32
|
+
s.add_development_dependency 'faraday', '~> 2.7', '>= 2.7.4'
|
25
33
|
s.add_development_dependency 'http', '~> 5.1', '>= 5.1.1'
|
34
|
+
s.add_development_dependency 'httparty', '~> 0.21.0'
|
35
|
+
s.add_development_dependency 'rest-client', '~> 2.1'
|
26
36
|
s.add_development_dependency 'rspec', '~> 3.12'
|
27
37
|
s.add_development_dependency 'webmock', '~> 3.18', '>= 3.18.1'
|
28
|
-
s.add_development_dependency 'faraday', '~> 2.7', '>= 2.7.4'
|
29
38
|
|
30
39
|
end
|
31
40
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: supergood
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Klarfeld
|
@@ -10,54 +10,202 @@ bindir: exe
|
|
10
10
|
cert_chain: []
|
11
11
|
date: 2023-03-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: base64
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: digest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.1'
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 3.1.1
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '3.1'
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 3.1.1
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: dotenv
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.8'
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 2.8.1
|
57
|
+
type: :runtime
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - "~>"
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '2.8'
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 2.8.1
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: json
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '2.6'
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 2.6.3
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '2.6'
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: 2.6.3
|
87
|
+
- !ruby/object:Gem::Dependency
|
88
|
+
name: logger
|
89
|
+
requirement: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - "~>"
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '1.5'
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.6'
|
97
|
+
type: :runtime
|
98
|
+
prerelease: false
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.5'
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '1.6'
|
13
107
|
- !ruby/object:Gem::Dependency
|
14
108
|
name: rudash
|
15
109
|
requirement: !ruby/object:Gem::Requirement
|
16
110
|
requirements:
|
17
111
|
- - "~>"
|
18
112
|
- !ruby/object:Gem::Version
|
19
|
-
version: '4.
|
113
|
+
version: '4.1'
|
20
114
|
- - ">="
|
21
115
|
- !ruby/object:Gem::Version
|
22
|
-
version: 4.
|
116
|
+
version: 4.1.2
|
23
117
|
type: :runtime
|
24
118
|
prerelease: false
|
25
119
|
version_requirements: !ruby/object:Gem::Requirement
|
26
120
|
requirements:
|
27
121
|
- - "~>"
|
28
122
|
- !ruby/object:Gem::Version
|
29
|
-
version: '4.
|
123
|
+
version: '4.1'
|
30
124
|
- - ">="
|
31
125
|
- !ruby/object:Gem::Version
|
32
|
-
version: 4.
|
126
|
+
version: 4.1.2
|
33
127
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
128
|
+
name: securerandom
|
35
129
|
requirement: !ruby/object:Gem::Requirement
|
36
130
|
requirements:
|
37
131
|
- - "~>"
|
38
132
|
- !ruby/object:Gem::Version
|
39
|
-
version: '2
|
40
|
-
|
133
|
+
version: '0.2'
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: 0.2.2
|
137
|
+
type: :runtime
|
41
138
|
prerelease: false
|
42
139
|
version_requirements: !ruby/object:Gem::Requirement
|
43
140
|
requirements:
|
44
141
|
- - "~>"
|
45
142
|
- !ruby/object:Gem::Version
|
46
|
-
version: '2
|
143
|
+
version: '0.2'
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: 0.2.2
|
47
147
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
148
|
+
name: stringio
|
49
149
|
requirement: !ruby/object:Gem::Requirement
|
50
150
|
requirements:
|
51
151
|
- - "~>"
|
52
152
|
- !ruby/object:Gem::Version
|
53
|
-
version:
|
153
|
+
version: '3.0'
|
154
|
+
type: :runtime
|
155
|
+
prerelease: false
|
156
|
+
version_requirements: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - "~>"
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '3.0'
|
161
|
+
- !ruby/object:Gem::Dependency
|
162
|
+
name: uri
|
163
|
+
requirement: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - "~>"
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0.12'
|
168
|
+
type: :runtime
|
169
|
+
prerelease: false
|
170
|
+
version_requirements: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - "~>"
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0.12'
|
175
|
+
- !ruby/object:Gem::Dependency
|
176
|
+
name: zlib
|
177
|
+
requirement: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - "~>"
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '3.0'
|
182
|
+
type: :runtime
|
183
|
+
prerelease: false
|
184
|
+
version_requirements: !ruby/object:Gem::Requirement
|
185
|
+
requirements:
|
186
|
+
- - "~>"
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
version: '3.0'
|
189
|
+
- !ruby/object:Gem::Dependency
|
190
|
+
name: faraday
|
191
|
+
requirement: !ruby/object:Gem::Requirement
|
192
|
+
requirements:
|
193
|
+
- - "~>"
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
version: '2.7'
|
196
|
+
- - ">="
|
197
|
+
- !ruby/object:Gem::Version
|
198
|
+
version: 2.7.4
|
54
199
|
type: :development
|
55
200
|
prerelease: false
|
56
201
|
version_requirements: !ruby/object:Gem::Requirement
|
57
202
|
requirements:
|
58
203
|
- - "~>"
|
59
204
|
- !ruby/object:Gem::Version
|
60
|
-
version:
|
205
|
+
version: '2.7'
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: 2.7.4
|
61
209
|
- !ruby/object:Gem::Dependency
|
62
210
|
name: http
|
63
211
|
requirement: !ruby/object:Gem::Requirement
|
@@ -79,59 +227,67 @@ dependencies:
|
|
79
227
|
- !ruby/object:Gem::Version
|
80
228
|
version: 5.1.1
|
81
229
|
- !ruby/object:Gem::Dependency
|
82
|
-
name:
|
230
|
+
name: httparty
|
83
231
|
requirement: !ruby/object:Gem::Requirement
|
84
232
|
requirements:
|
85
233
|
- - "~>"
|
86
234
|
- !ruby/object:Gem::Version
|
87
|
-
version:
|
235
|
+
version: 0.21.0
|
88
236
|
type: :development
|
89
237
|
prerelease: false
|
90
238
|
version_requirements: !ruby/object:Gem::Requirement
|
91
239
|
requirements:
|
92
240
|
- - "~>"
|
93
241
|
- !ruby/object:Gem::Version
|
94
|
-
version:
|
242
|
+
version: 0.21.0
|
95
243
|
- !ruby/object:Gem::Dependency
|
96
|
-
name:
|
244
|
+
name: rest-client
|
97
245
|
requirement: !ruby/object:Gem::Requirement
|
98
246
|
requirements:
|
99
247
|
- - "~>"
|
100
248
|
- !ruby/object:Gem::Version
|
101
|
-
version: '
|
102
|
-
- - ">="
|
103
|
-
- !ruby/object:Gem::Version
|
104
|
-
version: 3.18.1
|
249
|
+
version: '2.1'
|
105
250
|
type: :development
|
106
251
|
prerelease: false
|
107
252
|
version_requirements: !ruby/object:Gem::Requirement
|
108
253
|
requirements:
|
109
254
|
- - "~>"
|
110
255
|
- !ruby/object:Gem::Version
|
111
|
-
version: '
|
112
|
-
|
256
|
+
version: '2.1'
|
257
|
+
- !ruby/object:Gem::Dependency
|
258
|
+
name: rspec
|
259
|
+
requirement: !ruby/object:Gem::Requirement
|
260
|
+
requirements:
|
261
|
+
- - "~>"
|
113
262
|
- !ruby/object:Gem::Version
|
114
|
-
version: 3.
|
263
|
+
version: '3.12'
|
264
|
+
type: :development
|
265
|
+
prerelease: false
|
266
|
+
version_requirements: !ruby/object:Gem::Requirement
|
267
|
+
requirements:
|
268
|
+
- - "~>"
|
269
|
+
- !ruby/object:Gem::Version
|
270
|
+
version: '3.12'
|
115
271
|
- !ruby/object:Gem::Dependency
|
116
|
-
name:
|
272
|
+
name: webmock
|
117
273
|
requirement: !ruby/object:Gem::Requirement
|
118
274
|
requirements:
|
119
275
|
- - "~>"
|
120
276
|
- !ruby/object:Gem::Version
|
121
|
-
version: '
|
277
|
+
version: '3.18'
|
122
278
|
- - ">="
|
123
279
|
- !ruby/object:Gem::Version
|
124
|
-
version:
|
280
|
+
version: 3.18.1
|
125
281
|
type: :development
|
126
282
|
prerelease: false
|
127
283
|
version_requirements: !ruby/object:Gem::Requirement
|
128
284
|
requirements:
|
129
285
|
- - "~>"
|
130
286
|
- !ruby/object:Gem::Version
|
131
|
-
version: '
|
287
|
+
version: '3.18'
|
132
288
|
- - ">="
|
133
289
|
- !ruby/object:Gem::Version
|
134
|
-
version:
|
290
|
+
version: 3.18.1
|
135
291
|
description:
|
136
292
|
email: alex@supergood.ai
|
137
293
|
executables: []
|