supergood 0.1.4 → 1.0.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/.vscode/settings.json +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/lib/supergood/api.rb +20 -11
- data/lib/supergood/client.rb +55 -30
- 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
- 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: 5ee319f07b495599aa2deafcf5b6c995402e55b4d7a58ddb82d55379aa6ba99a
|
4
|
+
data.tar.gz: 15ab686e3bec0c37771e8fa2892f7cdb69312595cc8726c0d690855681eeb26d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11eed3208aeec8618d71cb7a832988ef0e88cfb1d55fed315b203418cb93ab4bfba251b22ef978afd0ec672b4b849d2af28ad03435d20003db1020e154faa74c
|
7
|
+
data.tar.gz: 583dacf60c012289be925e8c49471a14d401be64a00ef0cf299e758797bd318dadb0bb3d3a64cb53ef925ba0620fe253213d6406e1ffc35e094c0c5b5e69a239
|
data/.vscode/settings.json
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -12,7 +12,7 @@ gem install supergood
|
|
12
12
|
|
13
13
|
**Environment variables**
|
14
14
|
|
15
|
-
Set the environment variables `SUPERGOOD_CLIENT_ID` and `SUPERGOOD_CLIENT_SECRET` using the API keys generated in the [getting started instructions](
|
15
|
+
Set the environment variables `SUPERGOOD_CLIENT_ID` and `SUPERGOOD_CLIENT_SECRET` using the API keys generated in the [getting started instructions](https://github.com/supergoodsystems/docs/blob/main/getting-started.md).
|
16
16
|
|
17
17
|
Initialize the Supergood client at the root of your application, or anywhere you're making API calls with the following code:
|
18
18
|
|
@@ -26,7 +26,7 @@ Supergood.init()
|
|
26
26
|
|
27
27
|
You can also pass the API keys in manually without setting environment variables.\
|
28
28
|
\
|
29
|
-
Replace `<CLIENT_ID>` and `<CLIENT_SECRET>` with the API keys you generated in the [getting started instructions](
|
29
|
+
Replace `<CLIENT_ID>` and `<CLIENT_SECRET>` with the API keys you generated in the [getting started instructions](https://github.com/supergoodsystems/docs/blob/main/getting-started.md).
|
30
30
|
|
31
31
|
```ruby
|
32
32
|
require 'supergood'
|
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
@@ -11,7 +11,7 @@ Dotenv.load
|
|
11
11
|
|
12
12
|
module Supergood
|
13
13
|
|
14
|
-
DEFAULT_SUPERGOOD_BASE_URL = 'https://
|
14
|
+
DEFAULT_SUPERGOOD_BASE_URL = 'https://api.supergood.ai'
|
15
15
|
class << self
|
16
16
|
def init(config={})
|
17
17
|
supergood_client_id = config[:client_id] || ENV['SUPERGOOD_CLIENT_ID']
|
@@ -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,14 +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
|
-
|
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])
|
123
142
|
cache_request(request_id, requested_at, request)
|
124
143
|
end
|
125
144
|
|
126
145
|
response = yield
|
127
|
-
if !ignored?(request[:domain]) && defined?(response)
|
146
|
+
if !ignore_endpoint && !ignored?(request[:domain]) && defined?(response)
|
128
147
|
cache_response(request_id, requested_at, response)
|
129
148
|
end
|
130
149
|
|
@@ -132,41 +151,47 @@ module Supergood
|
|
132
151
|
end
|
133
152
|
|
134
153
|
def cache_request(request_id, requested_at, request)
|
154
|
+
if !@config[:remote_config]
|
155
|
+
return
|
156
|
+
end
|
157
|
+
|
135
158
|
begin
|
136
159
|
request_payload = {
|
137
|
-
id
|
138
|
-
headers
|
139
|
-
method
|
140
|
-
url
|
141
|
-
path
|
142
|
-
search
|
143
|
-
body
|
144
|
-
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
|
145
168
|
}
|
146
169
|
@request_cache[request_id] = {
|
147
|
-
request
|
170
|
+
'request' => request_payload
|
148
171
|
}
|
149
172
|
rescue => e
|
150
|
-
log.error({ request
|
173
|
+
log.error({ 'request' => request }, e, ERRORS[:CACHING_REQUEST])
|
151
174
|
end
|
152
175
|
end
|
153
176
|
|
154
177
|
def cache_response(request_id, requested_at, response)
|
178
|
+
if !@config[:remote_config]
|
179
|
+
return
|
180
|
+
end
|
181
|
+
|
155
182
|
begin
|
156
183
|
responded_at = Time.now
|
157
184
|
duration = (responded_at - requested_at) * 1000
|
158
185
|
request_payload = @request_cache[request_id]
|
159
186
|
response_payload = {
|
160
|
-
headers
|
161
|
-
status
|
162
|
-
statusText
|
163
|
-
body
|
164
|
-
respondedAt
|
165
|
-
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
|
166
193
|
}
|
167
|
-
@response_cache[request_id] =
|
168
|
-
response: response_payload
|
169
|
-
}), @keys_to_hash)
|
194
|
+
@response_cache[request_id] = request_payload.merge({ 'response' => response_payload })
|
170
195
|
@request_cache.delete(request_id)
|
171
196
|
rescue => e
|
172
197
|
log.error(
|
@@ -177,7 +202,7 @@ module Supergood
|
|
177
202
|
end
|
178
203
|
|
179
204
|
def ignored?(domain)
|
180
|
-
base_domain =
|
205
|
+
base_domain = Supergood::Utils.get_host_without_www(@base_url)
|
181
206
|
if domain == base_domain
|
182
207
|
return true
|
183
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