cased-ruby 0.3.3

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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rubocop.yml +21 -0
  3. data/.github/workflows/ruby.yml +46 -0
  4. data/.gitignore +10 -0
  5. data/.rubocop.yml +88 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +7 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +8 -0
  10. data/Gemfile.lock +107 -0
  11. data/LICENSE +21 -0
  12. data/README.md +661 -0
  13. data/Rakefile +12 -0
  14. data/bin/console +15 -0
  15. data/bin/rubocop +29 -0
  16. data/cased-ruby.gemspec +48 -0
  17. data/lib/cased-ruby.rb +3 -0
  18. data/lib/cased.rb +260 -0
  19. data/lib/cased/clients.rb +21 -0
  20. data/lib/cased/collection_response.rb +117 -0
  21. data/lib/cased/config.rb +172 -0
  22. data/lib/cased/context.rb +50 -0
  23. data/lib/cased/context/expander.rb +33 -0
  24. data/lib/cased/error.rb +8 -0
  25. data/lib/cased/http/client.rb +83 -0
  26. data/lib/cased/http/error.rb +99 -0
  27. data/lib/cased/instrumentation/controller.rb +34 -0
  28. data/lib/cased/instrumentation/log_subscriber.rb +31 -0
  29. data/lib/cased/integrations/sidekiq.rb +17 -0
  30. data/lib/cased/integrations/sidekiq/client_middleware.rb +14 -0
  31. data/lib/cased/integrations/sidekiq/server_middleware.rb +20 -0
  32. data/lib/cased/model.rb +98 -0
  33. data/lib/cased/policy.rb +24 -0
  34. data/lib/cased/publishers.rb +6 -0
  35. data/lib/cased/publishers/active_support_publisher.rb +16 -0
  36. data/lib/cased/publishers/base.rb +17 -0
  37. data/lib/cased/publishers/error.rb +11 -0
  38. data/lib/cased/publishers/http_publisher.rb +15 -0
  39. data/lib/cased/publishers/null_publisher.rb +11 -0
  40. data/lib/cased/publishers/test_publisher.rb +19 -0
  41. data/lib/cased/query.rb +87 -0
  42. data/lib/cased/rack_middleware.rb +15 -0
  43. data/lib/cased/response.rb +37 -0
  44. data/lib/cased/sensitive.rb +4 -0
  45. data/lib/cased/sensitive/handler.rb +54 -0
  46. data/lib/cased/sensitive/processor.rb +78 -0
  47. data/lib/cased/sensitive/range.rb +54 -0
  48. data/lib/cased/sensitive/result.rb +8 -0
  49. data/lib/cased/sensitive/string.rb +43 -0
  50. data/lib/cased/test_helper.rb +188 -0
  51. data/lib/cased/version.rb +5 -0
  52. data/vendor/cache/activesupport-6.0.3.4.gem +0 -0
  53. data/vendor/cache/addressable-2.7.0.gem +0 -0
  54. data/vendor/cache/ast-2.4.0.gem +0 -0
  55. data/vendor/cache/byebug-11.0.1.gem +0 -0
  56. data/vendor/cache/concurrent-ruby-1.1.7.gem +0 -0
  57. data/vendor/cache/connection_pool-2.2.2.gem +0 -0
  58. data/vendor/cache/crack-0.4.3.gem +0 -0
  59. data/vendor/cache/docile-1.3.2.gem +0 -0
  60. data/vendor/cache/dotpath-0.1.0.gem +0 -0
  61. data/vendor/cache/faraday-1.1.0.gem +0 -0
  62. data/vendor/cache/faraday_middleware-1.0.0.gem +0 -0
  63. data/vendor/cache/hashdiff-1.0.1.gem +0 -0
  64. data/vendor/cache/i18n-1.8.5.gem +0 -0
  65. data/vendor/cache/jaro_winkler-1.5.4.gem +0 -0
  66. data/vendor/cache/json-2.3.1.gem +0 -0
  67. data/vendor/cache/minitest-5.13.0.gem +0 -0
  68. data/vendor/cache/mocha-1.11.2.gem +0 -0
  69. data/vendor/cache/multipart-post-2.1.1.gem +0 -0
  70. data/vendor/cache/net-http-persistent-3.1.0.gem +0 -0
  71. data/vendor/cache/parallel-1.19.1.gem +0 -0
  72. data/vendor/cache/parser-2.7.1.3.gem +0 -0
  73. data/vendor/cache/public_suffix-4.0.5.gem +0 -0
  74. data/vendor/cache/rack-2.2.2.gem +0 -0
  75. data/vendor/cache/rack-protection-2.0.8.1.gem +0 -0
  76. data/vendor/cache/rainbow-3.0.0.gem +0 -0
  77. data/vendor/cache/rake-10.5.0.gem +0 -0
  78. data/vendor/cache/redis-4.1.4.gem +0 -0
  79. data/vendor/cache/rubocop-0.78.0.gem +0 -0
  80. data/vendor/cache/rubocop-performance-1.5.2.gem +0 -0
  81. data/vendor/cache/ruby-progressbar-1.10.1.gem +0 -0
  82. data/vendor/cache/ruby2_keywords-0.0.2.gem +0 -0
  83. data/vendor/cache/safe_yaml-1.0.5.gem +0 -0
  84. data/vendor/cache/sidekiq-6.0.7.gem +0 -0
  85. data/vendor/cache/simplecov-0.18.5.gem +0 -0
  86. data/vendor/cache/simplecov-html-0.12.2.gem +0 -0
  87. data/vendor/cache/thread_safe-0.3.6.gem +0 -0
  88. data/vendor/cache/tzinfo-1.2.7.gem +0 -0
  89. data/vendor/cache/unicode-display_width-1.6.1.gem +0 -0
  90. data/vendor/cache/webmock-3.8.3.gem +0 -0
  91. data/vendor/cache/yard-0.9.24.gem +0 -0
  92. data/vendor/cache/zeitwerk-2.4.0.gem +0 -0
  93. metadata +375 -0
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cased
4
+ class Response
5
+ attr_reader :body
6
+ attr_reader :exception
7
+
8
+ def initialize(response: nil, exception: nil)
9
+ @response = response
10
+ @body = response&.body
11
+ @exception = exception
12
+ end
13
+
14
+ def error
15
+ @exception.presence || (body && body['error']).presence
16
+ end
17
+
18
+ def error?
19
+ # If there was an exception during the execution of the request.
20
+ return true if @exception.present?
21
+
22
+ # If the HTTP response was outside of 200-299
23
+ return true unless @response.success?
24
+
25
+ # If the HTTP response contained an error key.
26
+ return true if body && body['error'].present?
27
+
28
+ false
29
+ end
30
+
31
+ def success?
32
+ return false if @response.nil?
33
+
34
+ @response.success?
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cased/sensitive/processor'
4
+ require 'cased/sensitive/handler'
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cased
4
+ module Sensitive
5
+ class Handler
6
+ def self.handlers
7
+ @handlers ||= []
8
+ end
9
+
10
+ class << self
11
+ attr_writer :handlers
12
+ end
13
+
14
+ def self.register(label, handler)
15
+ handlers << Handler.new(label, handler)
16
+ end
17
+
18
+ attr_reader :label
19
+
20
+ def initialize(label, handler)
21
+ @label = label.to_sym
22
+ @handler = prepare_handler(handler)
23
+ end
24
+
25
+ def call(audit_event, key, value)
26
+ @handler.call(audit_event, key.to_sym, value)
27
+ end
28
+
29
+ private
30
+
31
+ def prepare_handler(handler)
32
+ case handler
33
+ when Regexp
34
+ proc do |_audit_event, key, value|
35
+ string = Cased::Sensitive::String.new(value)
36
+ string.matches(handler).collect do |match|
37
+ begin_offset = match.begin(0)
38
+ end_offset = match.end(0)
39
+
40
+ Cased::Sensitive::Range.new(
41
+ label: label,
42
+ key: key,
43
+ begin_offset: begin_offset,
44
+ end_offset: end_offset,
45
+ )
46
+ end
47
+ end
48
+ else
49
+ raise ArgumentError, "expected #{handler} to be a Regexp or Proc"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cased/sensitive/string'
4
+
5
+ module Cased
6
+ module Sensitive
7
+ class Processor
8
+ def self.process(audit_event, handlers = nil)
9
+ handlers ||= Cased::Sensitive::Handler.handlers
10
+ processor = new(audit_event, handlers)
11
+ processor.process
12
+ processor
13
+ end
14
+
15
+ def self.process!(audit_event, handlers = nil)
16
+ processor = process(audit_event, handlers)
17
+ return unless processor.sensitive?
18
+
19
+ audit_event[:'.cased'] = {
20
+ pii: processor.to_h,
21
+ }
22
+ end
23
+
24
+ attr_reader :audit_event
25
+ attr_reader :handlers
26
+
27
+ def initialize(audit_event, handlers)
28
+ @audit_event = audit_event.dup.freeze
29
+ @ranges = []
30
+ @handlers = handlers
31
+ end
32
+
33
+ def process
34
+ return true if defined?(@processed)
35
+
36
+ walk(audit_event)
37
+ @processed = true
38
+ end
39
+
40
+ def ranges
41
+ @ranges.flatten
42
+ end
43
+
44
+ def sensitive?
45
+ process && ranges.any?
46
+ end
47
+
48
+ def to_h
49
+ results = {}
50
+ ranges.each do |range|
51
+ results[range.key] ||= []
52
+ results[range.key] << range.to_h
53
+ end
54
+ results
55
+ end
56
+
57
+ private
58
+
59
+ def walk(hash)
60
+ hash.each_with_json_path do |path, value|
61
+ case value
62
+ when Cased::Sensitive::String
63
+ @ranges << value.range(key: path)
64
+ when ::String
65
+ process_handlers(audit_event, path, value)
66
+ end
67
+ end
68
+ end
69
+
70
+ def process_handlers(audit_event, path, value)
71
+ handlers.each do |handler|
72
+ ranges = handler.call(audit_event, path, value)
73
+ @ranges << ranges unless ranges.nil? || ranges.empty?
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cased
4
+ module Sensitive
5
+ class Range
6
+ # Public: The human label describing what sensitive information was
7
+ # label. Username, email, date of birth, etc.
8
+ attr_reader :label
9
+
10
+ # Public: The JSON key.
11
+ attr_reader :key
12
+
13
+ # Public: This is the identifier that groups sensitive ranges together.
14
+ # This could be an identifier to an individual for example.
15
+ attr_reader :identifier
16
+
17
+ # Public: The beginning offset of the sensitive value in the original value.
18
+ attr_reader :begin_offset
19
+
20
+ # Public: The end offset of the sensitive value in the original value.
21
+ attr_reader :end_offset
22
+
23
+ def initialize(label: nil, key:, begin_offset:, end_offset:, identifier: nil)
24
+ raise ArgumentError, 'missing key' if key.nil?
25
+ raise ArgumentError, 'missing begin_offset' if begin_offset.nil?
26
+ raise ArgumentError, 'missing end_offset' if end_offset.nil?
27
+
28
+ @label = label
29
+ @key = key
30
+ @identifier = identifier
31
+ @begin_offset = begin_offset
32
+ @end_offset = end_offset
33
+ end
34
+
35
+ def ==(other)
36
+ @begin_offset == other.begin_offset &&
37
+ @end_offset == other.end_offset &&
38
+ @label == other.label &&
39
+ @key == other.key &&
40
+ @identifier == other.identifier
41
+ end
42
+
43
+ def to_h
44
+ {
45
+ begin: @begin_offset,
46
+ end: @end_offset,
47
+ }.tap do |hash|
48
+ hash[:label] = label if label
49
+ hash[:identifier] = identifier if identifier
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cased
4
+ module Sensitive
5
+ class Result
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cased/sensitive/range'
4
+
5
+ module Cased
6
+ module Sensitive
7
+ class String < String
8
+ attr_reader :label
9
+ attr_reader :string
10
+
11
+ def initialize(string, label: nil)
12
+ super(string)
13
+ @label = label
14
+ end
15
+
16
+ def range(key:)
17
+ Cased::Sensitive::Range.new(
18
+ label: label,
19
+ key: key,
20
+ begin_offset: 0,
21
+ end_offset: length,
22
+ )
23
+ end
24
+
25
+ def matches(regex)
26
+ offset = 0
27
+ matches = []
28
+
29
+ while (result = match(regex, offset))
30
+ matches.push(result)
31
+ offset = result.end(0)
32
+ end
33
+
34
+ matches
35
+ end
36
+
37
+ def ==(other)
38
+ super(other) &&
39
+ @label == other.label
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash/keys'
4
+
5
+ module Cased
6
+ module TestHelper
7
+ def before_setup
8
+ @original_cased_publishers = Cased.publishers
9
+ Cased.publishers = [
10
+ cased_test_publisher,
11
+ ]
12
+
13
+ clear_cased_events
14
+ clear_cased_context
15
+ super
16
+ end
17
+
18
+ def after_teardown
19
+ super
20
+
21
+ Cased.publishers = @original_cased_publishers
22
+ end
23
+
24
+ # Clears all published events in the test Cased publisher
25
+ def clear_cased_events
26
+ cased_events.clear
27
+ end
28
+
29
+ def clear_cased_context
30
+ Cased::Context.clear!
31
+ end
32
+
33
+ def cased_events
34
+ cased_test_publisher.events
35
+ end
36
+
37
+ # Assertion that helps with testing that a number of events have been published to Cased.
38
+ #
39
+ # @param expected_event_count [Integer] The number of expected Cased events to be published.
40
+ # @param expected_event_body [Hash] Expected event to be published to Cased.
41
+ #
42
+ # @example Expected events with a filter inside of a block
43
+ # def test_creates_user_create_event
44
+ # assert_cased_events 1, action: 'user.create' do
45
+ # create(:user)
46
+ # end
47
+ # end
48
+ #
49
+ # @example Expected events without a filter inside of a block
50
+ # def test_creates_user_create_event
51
+ # assert_cased_events 1 do
52
+ # create(:user)
53
+ # end
54
+ # end
55
+ #
56
+ # @example Expected events with a filter for the duration of the test
57
+ # def test_creates_user_create_event
58
+ # create(:user)
59
+ #
60
+ # assert_cased_events 1, action: 'user.create'
61
+ # end
62
+ #
63
+ # @example Expected events without a filter for the duration of the test
64
+ # def test_creates_user_create_event
65
+ # create(:user)
66
+ #
67
+ # assert_cased_events 1
68
+ # end
69
+ #
70
+ # @example Cased::Model value hash
71
+ # def test_creates_user_create_event
72
+ # user = create(:user)
73
+ #
74
+ # assert_cased_events 1, action: 'user.login', user: user
75
+ # end
76
+ #
77
+ # @return [void]
78
+ def assert_cased_events(expected_event_count, expected_event_body = nil, &block)
79
+ expected_event_body&.deep_symbolize_keys!
80
+
81
+ actual_event_count = if block
82
+ events_before_block = cased_events_with(expected_event_body)
83
+
84
+ block&.call
85
+
86
+ events_after_block = cased_events_with(expected_event_body)
87
+
88
+ events_after_block.length - events_before_block.length
89
+ else
90
+ cased_events_with(expected_event_body).length
91
+ end
92
+
93
+ assert_equal expected_event_count, actual_event_count, "#{expected_event_count} Cased published events expected, but #{actual_event_count} were published"
94
+ end
95
+
96
+ # Assertion that expects there to have been zero matching Cased events.
97
+ #
98
+ # @param expected_event_body [Hash] Expected event not to be published to Cased.
99
+ #
100
+ # @example Expected no events with a filter inside of a block
101
+ # def test_creates_bot_account
102
+ # assert_no_cased_events action: 'bot.create' do
103
+ # create(:bot)
104
+ # end
105
+ # end
106
+ #
107
+ # @example Expected no events inside of a block
108
+ # def test_creates_bot_account
109
+ # assert_no_cased_events do
110
+ # create(:bot)
111
+ # end
112
+ # end
113
+ #
114
+ # @example Expected no events containing a subset of the event body for the duration of the test
115
+ # def test_creates_bot_account
116
+ # create(:bot)
117
+ #
118
+ # assert_no_cased_events action: 'bot.create'
119
+ # end
120
+ #
121
+ # @example Expected no events for the duration of the test
122
+ # def test_creates_bot_account
123
+ # create(:bot)
124
+ #
125
+ # assert_no_cased_events
126
+ # end
127
+ #
128
+ # @return [void]
129
+ def assert_no_cased_events(expected_event_body = nil, &block)
130
+ assert_cased_events(0, expected_event_body, &block)
131
+ end
132
+
133
+ # Locates all published events matching a particular shape.
134
+ #
135
+ # @param expected_event [Hash] the shape of event expected to be published to Cased
136
+ #
137
+ # @example Simple hash
138
+ # cased_events_with(action: 'user.login') # => [{ action: 'user.login', actor: 'garrett@cased.com' }, { action: 'user.login', actor: 'ted@cased.com' }]
139
+ #
140
+ # @example Nested hash
141
+ # cased_events_with(issues: [{ issue_id: 1 }]) # => [{ action: 'user.login', issues: [{ issue_id: 1 }, { issue_id: 2 }]}]
142
+ #
143
+ # @example Cased::Model value hash
144
+ # user = User.new
145
+ # user.cased_context # => { user: 'garrett@cased.com', user_id: 'user_1234' }
146
+ # cased_events_with(user: user) # => [{ user: 'garrett@cased.com', user_id: 'user_1234' }]
147
+ #
148
+ # @return [Array<Hash>] Array of matching published Cased events.
149
+ # @raises [ArgumentError] if expected_event is empty.
150
+ def cased_events_with(expected_event = {})
151
+ return cased_events.dup if expected_event.nil?
152
+
153
+ if expected_event.empty?
154
+ raise ArgumentError, 'You must call cased_events_with with a non empty Hash otherwise it will match all events'
155
+ end
156
+
157
+ expanded_expected_event = Cased::Context::Expander.expand(expected_event)
158
+ if expanded_expected_event.empty?
159
+ raise ArgumentError, <<~MSG.strip
160
+ cased_events_with would have matched any published Cased event.
161
+
162
+ cased_events_with was called with #{expected_event.inspect} but resulted into #{expanded_expected_event} after it was expanded.
163
+
164
+ This typically happens when an object that includes Cased::Model does not implement either the #cased_id or #to_s method.
165
+ MSG
166
+ end
167
+
168
+ # We need to normalize input as it could be a mix of strings and symbols.
169
+ expected_event.deep_symbolize_keys!
170
+ expanded_expected_event = expanded_expected_event.to_a
171
+
172
+ events = cased_events.dup.collect(&:deep_symbolize_keys).collect(&:to_a)
173
+ matching_events = events.select do |event|
174
+ diff = expanded_expected_event - event
175
+ diff.empty?
176
+ end
177
+
178
+ matching_events.collect(&:to_h)
179
+ end
180
+
181
+ # The test published used for the duration of the test.
182
+ #
183
+ # @return [Cased::Publishers::TestPublisher]
184
+ def cased_test_publisher
185
+ @cased_test_publisher ||= Cased::Publishers::TestPublisher.new
186
+ end
187
+ end
188
+ end