cased-ruby 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
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