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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a20e9c78ef66594eaed117db23b1581b3531497aaf3a087ad92466f76a8e07a8
4
- data.tar.gz: 22e62c82c3cea5bfba10a8d5199ebf1a385bab78861c6f20efcd55921e65eee0
3
+ metadata.gz: cc2f971650d5a6065d80fbecdea2f80724f332797f246a4c81dbc8e07bfd1c1b
4
+ data.tar.gz: fc66b646323cf6dab01694e383038e009eb2667d88eec12056ce50908d10042a
5
5
  SHA512:
6
- metadata.gz: da801337729a2975cd42231da0de2e773aafdcc0a391a69111b4acb55dfe1d8126e1017e72481688dcf410d23a072c51204e9885e8938d9903ed1f947de94bfc
7
- data.tar.gz: b9848d4363bcb7b04bb19b46dc61f8adcb8bbf9b911664a3ba367721f171e2ad950c3fdcb3d236192fe5c160e4d33aa926615583647c7b3e44b0038fc4ed6c27
6
+ metadata.gz: 95aed28c6df8865a06edd94999b556a8b2956187b7e78503496102e4d512174ba7164dd2cb587ef37246905dabdb8b44ed672393829e8bf33b4a7d8cb7f47bdb
7
+ data.tar.gz: f69d675f69015838077bb51d215ad9806e82c6ad12c33f4bcc36dd0aac58098e28120ac29addbcbb981cb0de8a31b49675abdb7a0a4e7dce28c74d30ebfa490d
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "editor.codeActionsOnSave": {
3
- "source.fixAll.eslint": true
3
+ "source.fixAll.eslint": "explicit"
4
4
  },
5
5
  "editor.tabSize": 2,
6
6
  }
data/Gemfile.lock CHANGED
@@ -1,31 +1,39 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- supergood (0.1.3)
5
- rudash (~> 4.0, >= 4.0.2)
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.1)
19
+ addressable (2.8.6)
11
20
  public_suffix (>= 2.0.2, < 6.0)
12
- base64 (0.1.1)
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.5.20190701)
18
- unf (>= 0.0.5, < 1.0.0)
26
+ domain_name (0.6.20240107)
19
27
  dotenv (2.8.1)
20
- faraday (2.7.4)
21
- faraday-net_http (>= 2.0, < 3.1)
22
- ruby2_keywords (>= 0.0.4)
23
- faraday-net_http (3.0.2)
24
- ffi (1.15.5)
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.1)
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.6.3)
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.5.3)
46
- mime-types (3.4.1)
53
+ logger (1.6.0)
54
+ mime-types (3.5.2)
47
55
  mime-types-data (~> 3.2015)
48
- mime-types-data (3.2023.0218.1)
49
- mini_mime (1.1.2)
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.1)
53
- rake (13.0.6)
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.5)
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.1)
74
+ rspec-core (3.12.2)
65
75
  rspec-support (~> 3.12.0)
66
- rspec-expectations (3.12.2)
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.4)
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.0)
73
- ruby2_keywords (0.0.5)
82
+ rspec-support (3.12.1)
74
83
  rudash (4.1.2)
75
- securerandom (0.2.2)
76
- stringio (3.0.5)
77
- unf (0.1.4)
78
- unf_ext
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.0.0)
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' => 'Basic ' + Base64.encode64(client_id + ':' + client_secret).gsub(/\n/, '')
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 + '/events')
34
+ uri = URI("#{@base_url}/events")
33
35
  response = Net::HTTP.post(uri, payload.to_json, @header_options)
34
- if response.code == '200'
35
- return JSON.parse(response.body, symbolize_names: true)
36
- elsif response.code == '401'
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 + '/errors')
51
+ uri = URI("#{@base_url}/errors")
49
52
  response = Net::HTTP.post(uri, payload.to_json, @header_options)
50
- if response.code == '200'
51
- return JSON.parse(response.body, symbolize_names: true)
52
- else
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
@@ -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
- patch_all()
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
- if !ignored?(request[:domain])
123
- puts "Caching Request"
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: request_id,
140
- headers: request[:headers],
141
- method: request[:method],
142
- url: request[:url],
143
- path: request[:path],
144
- search: request[:search] || '',
145
- body: Supergood::Utils.safe_parse_json(request[:body]),
146
- requestedAt: requested_at
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: request_payload
170
+ 'request' => request_payload
150
171
  }
151
172
  rescue => e
152
- log.error({ request: request }, e, ERRORS[:CACHING_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: response[:headers],
163
- status: response[:status],
164
- statusText: response[:statusText],
165
- body: Supergood::Utils.safe_parse_json(response[:body]),
166
- respondedAt: responded_at,
167
- duration: duration.round,
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] = Supergood::Utils.hash_values_from_keys(request_payload.merge({
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 = URI.parse(@base_url).hostname
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?
@@ -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 Purpos es',
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 = 500000
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'
@@ -13,7 +13,9 @@ module Supergood
13
13
  end
14
14
 
15
15
  def error(data, error, msg)
16
- super(error)
16
+ if(ENV['SUPERGOOD_LOG_LEVEL'] == 'debug')
17
+ super(error)
18
+ end
17
19
  @api.post_errors(
18
20
  {
19
21
  error: error.backtrace.join('\n'),
@@ -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
- return _obj
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
- return JSON.parse(input)
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#{"s" if http.use_ssl?}://#{http.address}#{request.path}")
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
- return DEFAULT_CONFIG.merge(config)
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)
@@ -1,3 +1,3 @@
1
1
  module Supergood
2
- VERSION = '0.1.5'.freeze
2
+ VERSION = '1.0.1'.freeze
3
3
  end
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.add_dependency 'rudash', '~> 4.0', '>= 4.0.2'
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 'rest-client', '~> 2.1'
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.5
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.0'
113
+ version: '4.1'
20
114
  - - ">="
21
115
  - !ruby/object:Gem::Version
22
- version: 4.0.2
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.0'
123
+ version: '4.1'
30
124
  - - ">="
31
125
  - !ruby/object:Gem::Version
32
- version: 4.0.2
126
+ version: 4.1.2
33
127
  - !ruby/object:Gem::Dependency
34
- name: rest-client
128
+ name: securerandom
35
129
  requirement: !ruby/object:Gem::Requirement
36
130
  requirements:
37
131
  - - "~>"
38
132
  - !ruby/object:Gem::Version
39
- version: '2.1'
40
- type: :development
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.1'
143
+ version: '0.2'
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: 0.2.2
47
147
  - !ruby/object:Gem::Dependency
48
- name: httparty
148
+ name: stringio
49
149
  requirement: !ruby/object:Gem::Requirement
50
150
  requirements:
51
151
  - - "~>"
52
152
  - !ruby/object:Gem::Version
53
- version: 0.21.0
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: 0.21.0
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: rspec
230
+ name: httparty
83
231
  requirement: !ruby/object:Gem::Requirement
84
232
  requirements:
85
233
  - - "~>"
86
234
  - !ruby/object:Gem::Version
87
- version: '3.12'
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: '3.12'
242
+ version: 0.21.0
95
243
  - !ruby/object:Gem::Dependency
96
- name: webmock
244
+ name: rest-client
97
245
  requirement: !ruby/object:Gem::Requirement
98
246
  requirements:
99
247
  - - "~>"
100
248
  - !ruby/object:Gem::Version
101
- version: '3.18'
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: '3.18'
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.18.1
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: faraday
272
+ name: webmock
117
273
  requirement: !ruby/object:Gem::Requirement
118
274
  requirements:
119
275
  - - "~>"
120
276
  - !ruby/object:Gem::Version
121
- version: '2.7'
277
+ version: '3.18'
122
278
  - - ">="
123
279
  - !ruby/object:Gem::Version
124
- version: 2.7.4
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: '2.7'
287
+ version: '3.18'
132
288
  - - ">="
133
289
  - !ruby/object:Gem::Version
134
- version: 2.7.4
290
+ version: 3.18.1
135
291
  description:
136
292
  email: alex@supergood.ai
137
293
  executables: []