logasm 0.2.6 → 0.3.0

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
  SHA1:
3
- metadata.gz: 1c37a9355e4d9426215d500f6fdb50a22954c2e4
4
- data.tar.gz: 58289a6f19d649b8c405638428a43938ca586132
3
+ metadata.gz: 440665fe491c115f43c411822de433ead259e193
4
+ data.tar.gz: 06568509dacd084a9607ac545b3cfd03447947db
5
5
  SHA512:
6
- metadata.gz: 6712eee9ed98800f0be30370509848b0c8a70b587e278fd63c7e742d5949aa0c3703b70cb17bc2c8a5d9d3cb02ab7e548c1d9a06c65a5f31711d3628edb55f7e
7
- data.tar.gz: e60591d1432b92075757b0fb76acbd7613f3f46fb6ec60af58bb2bd0e54331a90f73e301b3338f8f3667f4feabc547821a8bbf99c613aac288f6193e4309cbff
6
+ metadata.gz: 380b1118c988bbf141b9c03a63ca3c68d33f1f8873709bd92d5e52188dfb3adf980edf7d040eca62c2bdbc66fb4a142e0906e2272aa278cceb4109d6c88e52a2
7
+ data.tar.gz: b93a280171c1123d5559ac630a5db52710cd429a7f5bbaddbb73885f7aa75d287d21b117ca3a8fe75b6a2399667c05bcb1631ce889795f2b493036f664742992
data/README.md CHANGED
@@ -61,3 +61,74 @@ logasm = Logasm.build('myApp', { stdout: nil, logstash: { host: "localhost", por
61
61
  ```
62
62
 
63
63
  When no loggers are specified, it creates a stdout logger by default.
64
+
65
+ ## Preprocessors
66
+
67
+ Preprocessors allow modification of log messages, prior to sending of the message to the configured logger(s).
68
+
69
+ ### Blacklist
70
+
71
+ Excludes or masks defined fields of the passed hash object.
72
+ You can specify the name of the field and which action to take on it.
73
+ Nested hashes of any level are preprocessed as well.
74
+
75
+ Available actions:
76
+
77
+ * exclude(`default`) - fully excludes the field and its value from the hash.
78
+ * mask - replaces every character from the original value with `*`. In case of `array`, `hash` or `boolean` value is replaced with one `*`.
79
+
80
+ #### Configuration
81
+
82
+ ```yaml
83
+ preprocessors:
84
+ blacklist:
85
+ fields:
86
+ - key: password
87
+ - key: phone
88
+ action: mask
89
+ ```
90
+
91
+ #### Usage
92
+
93
+ ```ruby
94
+ logger = Logasm.build(application_name, logger_config, preprocessors)
95
+
96
+ input = {password: 'password', info: {phone: '+12055555555'}}
97
+
98
+ logger.debug("Received request", input)
99
+ ```
100
+
101
+ Logger output:
102
+
103
+ ```
104
+ Received request {"info":{"phone":"************"}}
105
+ ```
106
+
107
+ ### Whitelist
108
+
109
+ Masks all the fields except those whitelisted in the configuration using [JSON Pointer](https://tools.ietf.org/html/rfc6901).
110
+ Only simple values(`string`, `number`, `boolean`) can be whitelisted.
111
+
112
+ #### Configuration
113
+
114
+ ```yaml
115
+ preprocessors:
116
+ whitelist:
117
+ pointers: ['/info/phone']
118
+ ```
119
+
120
+ #### Usage
121
+
122
+ ```ruby
123
+ logger = Logasm.build(application_name, logger_config, preprocessors)
124
+
125
+ input = {password: 'password', info: {phone: '+12055555555'}}
126
+
127
+ logger.debug("Received request", input)
128
+ ```
129
+
130
+ Logger output:
131
+
132
+ ```
133
+ Received request {password: "********", "info":{"phone":"+12055555555"}}
134
+ ```
@@ -0,0 +1,67 @@
1
+ class Logasm
2
+ module Preprocessors
3
+ class Blacklist
4
+
5
+ DEFAULT_ACTION = 'exclude'
6
+ MASK_SYMBOL = '*'
7
+
8
+ class UnsupportedActionException < Exception
9
+ end
10
+
11
+ def initialize(config = {})
12
+ @fields_to_process = config[:fields].inject({}) do |mem, field|
13
+ key = field.delete(:key)
14
+ options = {action: DEFAULT_ACTION}.merge(field)
15
+ validate_action_supported(options[:action])
16
+ mem.merge(key => options)
17
+ end
18
+ end
19
+
20
+ def process(data)
21
+ if data.is_a? Hash
22
+ data.inject({}) do |mem, (key, val)|
23
+ if (field = @fields_to_process[key.to_s])
24
+ self.send(action_method(field[:action]), mem, key, val)
25
+ else
26
+ mem.merge(key => process(val))
27
+ end
28
+ end
29
+ elsif data.is_a? Array
30
+ data.inject([]) do |mem, val|
31
+ mem + [process(val)]
32
+ end
33
+ else
34
+ data
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def action_method(action)
41
+ "#{action}_field"
42
+ end
43
+
44
+ def validate_action_supported(action)
45
+ unless self.respond_to?(action_method(action).to_sym, true)
46
+ raise UnsupportedActionException.new("Action: #{action} is not supported")
47
+ end
48
+ end
49
+
50
+ def mask_field(data, key, val)
51
+ if val.is_a?(Hash) || val.is_a?(Array) || is_boolean?(val)
52
+ data.merge(key => MASK_SYMBOL)
53
+ else
54
+ data.merge(key => MASK_SYMBOL * val.to_s.length)
55
+ end
56
+ end
57
+
58
+ def exclude_field(data, *)
59
+ data
60
+ end
61
+
62
+ def is_boolean?(val)
63
+ val.is_a?(TrueClass) || val.is_a?(FalseClass)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,88 @@
1
+ class Logasm
2
+ module Preprocessors
3
+ class Whitelist
4
+
5
+ DEFAULT_WHITELIST = %w(/id /message /correlation_id /queue)
6
+ MASK_SYMBOL = '*'
7
+
8
+ class InvalidPointerFormatException < Exception
9
+ end
10
+
11
+ def initialize(config = {})
12
+ pointers = (config[:pointers] || []) + DEFAULT_WHITELIST
13
+ @fields_to_include = pointers.inject({}) do |mem, pointer|
14
+ validate_pointer(pointer)
15
+ mem.merge(decode(pointer) => true)
16
+ end
17
+ end
18
+
19
+ def process(data)
20
+ process_data('', data)
21
+ end
22
+
23
+ private
24
+
25
+ def validate_pointer(pointer)
26
+ if pointer.slice(-1) == '/'
27
+ raise InvalidPointerFormatException.new('Pointer should not contain trailing slash')
28
+ end
29
+ end
30
+
31
+ def decode(pointer)
32
+ pointer
33
+ .gsub('~0', '~')
34
+ .gsub('~1', '/')
35
+ end
36
+
37
+ def process_data(parent_pointer, data)
38
+ self.send("process_#{get_type(data)}", parent_pointer, data)
39
+ end
40
+
41
+ def get_type(data)
42
+ if data.is_a? Hash
43
+ 'hash'
44
+ elsif data.is_a? Array
45
+ 'array'
46
+ else
47
+ 'value'
48
+ end
49
+ end
50
+
51
+ def process_hash(parent_pointer, hash)
52
+ hash.inject({}) do |mem, (key, value)|
53
+ pointer = "#{parent_pointer}/#{key}"
54
+ processed_value = process_data(pointer, value)
55
+ mem.merge(key => processed_value)
56
+ end
57
+ end
58
+
59
+ def process_array(parent_pointer, array)
60
+ array.each_with_index.inject([]) do |mem, (value, index)|
61
+ pointer = "#{parent_pointer}/#{index}"
62
+ processed_value = process_data(pointer, value)
63
+ mem + [processed_value]
64
+ end
65
+ end
66
+
67
+ def process_value(parent_pointer, value)
68
+ if @fields_to_include[parent_pointer]
69
+ value
70
+ else
71
+ mask value
72
+ end
73
+ end
74
+
75
+ def mask(value)
76
+ if value && value.respond_to?(:to_s) && !is_boolean?(value)
77
+ MASK_SYMBOL * value.to_s.length
78
+ else
79
+ MASK_SYMBOL
80
+ end
81
+ end
82
+
83
+ def is_boolean?(value)
84
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,9 @@
1
+ class Logasm
2
+ module Preprocessors
3
+ def self.get(type, arguments)
4
+ require_relative "preprocessors/#{type.to_s}"
5
+ preprocessor = const_get(Inflecto.camelize(type.to_s))
6
+ preprocessor.new(arguments)
7
+ end
8
+ end
9
+ end
data/lib/logasm.rb CHANGED
@@ -4,20 +4,25 @@ require 'json'
4
4
  require_relative 'logasm/adapters'
5
5
  require_relative 'logasm/utils'
6
6
  require_relative 'logasm/null_logger'
7
+ require_relative 'logasm/preprocessors'
7
8
 
8
9
  LOG_LEVEL_QUERY_METHODS = [:debug?, :info?, :warn?, :error?, :fatal?]
9
10
 
10
11
  class Logasm
11
- def self.build(service_name, loggers_config)
12
+ def self.build(service_name, loggers_config, preprocessors_config = {})
12
13
  loggers_config ||= {stdout: nil}
14
+ preprocessors = preprocessors_config.map do |type, arguments|
15
+ Preprocessors.get(type.to_s, arguments || {})
16
+ end
13
17
  adapters = loggers_config.map do |type, arguments|
14
18
  Adapters.get(type.to_s, service_name, arguments || {})
15
19
  end
16
- new(adapters)
20
+ new(adapters, preprocessors)
17
21
  end
18
22
 
19
- def initialize(adapters)
23
+ def initialize(adapters, preprocessors)
20
24
  @adapters = adapters
25
+ @preprocessors = preprocessors
21
26
  end
22
27
 
23
28
  def debug(*args)
@@ -50,9 +55,16 @@ class Logasm
50
55
 
51
56
  def log(level, *args)
52
57
  data = parse_log_data(*args)
58
+ processed_data = preprocess(data)
53
59
 
54
60
  @adapters.each do |adapter|
55
- adapter.log(level, data)
61
+ adapter.log(level, processed_data)
62
+ end
63
+ end
64
+
65
+ def preprocess(data)
66
+ @preprocessors.inject(data) do |data_to_process, preprocessor|
67
+ preprocessor.process(data_to_process)
56
68
  end
57
69
  end
58
70
 
data/logasm.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "logasm"
7
- gem.version = '0.2.6'
7
+ gem.version = '0.3.0'
8
8
  gem.authors = ["Salemove"]
9
9
  gem.email = ["support@salemove.com"]
10
10
  gem.description = %q{It's logasmic}
data/spec/logasm_spec.rb CHANGED
@@ -38,11 +38,36 @@ describe Logasm do
38
38
 
39
39
  described_class.build('test_service', nil)
40
40
  end
41
+
42
+ it 'creates preprocessor when preprocessor defined' do
43
+ expect(described_class).to receive(:new) do |adapters, preprocessors|
44
+ expect(preprocessors.count).to be(1)
45
+ expect(preprocessors.first).to be_a(described_class::Preprocessors::Blacklist)
46
+ end
47
+
48
+ preprocessors = {blacklist: {fields: []}}
49
+ described_class.build('test_service', nil, preprocessors)
50
+ end
51
+ end
52
+
53
+ context 'when preprocessor defined' do
54
+ let(:logasm) { described_class.new([adapter], [preprocessor]) }
55
+ let(:adapter) { double }
56
+ let(:preprocessor) { double }
57
+ let(:data) { {data: 'data'} }
58
+
59
+ it 'preprocesses data before logging' do
60
+ expect(preprocessor).to receive(:process).with(data).and_return(data.merge(processed: true)).ordered
61
+ expect(adapter).to receive(:log).with(:info, data.merge(processed: true)).ordered
62
+
63
+ logasm.info(data)
64
+ end
41
65
  end
42
66
 
43
67
  context 'when parsing log data' do
44
- let(:logasm) { described_class.new([adapter]) }
68
+ let(:logasm) { described_class.new([adapter], preprocessors) }
45
69
  let(:adapter) { double }
70
+ let(:preprocessors) { [] }
46
71
 
47
72
  it 'parses empty string with nil metadata' do
48
73
  expect(adapter).to receive(:log).with(:info, message: '')
@@ -114,4 +139,10 @@ describe Logasm do
114
139
  end
115
140
  end
116
141
  end
142
+
143
+ it 'has the same interface as Ruby logger' do
144
+ skip "https://salemove.atlassian.net/browse/INF-464"
145
+ logger = described_class.build('test_service', stdout: {level: 'debug'})
146
+ expect(logger).to implement_interface(Logger)
147
+ end
117
148
  end
@@ -0,0 +1,131 @@
1
+ require 'spec_helper'
2
+ require_relative '../../lib/logasm/preprocessors/blacklist'
3
+
4
+ describe Logasm::Preprocessors::Blacklist do
5
+ subject(:processed_data) { described_class.new(config).process(data) }
6
+
7
+ let(:config) {{
8
+ fields: [{ key: 'field', action: action }]
9
+ }}
10
+
11
+ let(:action) {}
12
+ let(:data) {{
13
+ field: value,
14
+ data: {
15
+ field: 'secret'
16
+ },
17
+ array: [{field: 'secret'}]
18
+ }}
19
+
20
+ let(:value) { 'secret' }
21
+
22
+ context 'when action is unsupported' do
23
+ let(:action) { 'reverse' }
24
+
25
+ it 'throws exception' do
26
+ expect { processed_data }.to raise_exception(Logasm::Preprocessors::Blacklist::UnsupportedActionException)
27
+ end
28
+ end
29
+
30
+ context 'when action is "exclude"' do
31
+ let(:action) { 'exclude' }
32
+
33
+ it 'removes the field' do
34
+ expect(processed_data).not_to include(:field)
35
+ end
36
+
37
+ it 'removes nested field' do
38
+ expect(processed_data).not_to include_at_depth({field: 'secret'}, 1)
39
+ end
40
+
41
+ it 'removes nested in array field' do
42
+ expect(processed_data[:array]).not_to include({field: 'secret'})
43
+ end
44
+
45
+ context 'when field is deeply nested' do
46
+ let(:depth) { 10 }
47
+ let(:data) { data_with_nested_field({field: 'secret'}, depth) }
48
+
49
+ it 'removes deeply nested field' do
50
+ expect(processed_data).not_to include_at_depth({field: 'secret'}, depth)
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'when action is "mask"' do
56
+ let(:action) { 'mask' }
57
+
58
+ it 'masks nested field' do
59
+ expect(processed_data).to include_at_depth({field: '******'}, 1)
60
+ end
61
+
62
+ it 'masks nested in array field' do
63
+ expect(processed_data[:array]).to include({field: '******'})
64
+ end
65
+
66
+ context 'when field is string' do
67
+ let(:value) { 'secret' }
68
+
69
+ it 'masks value with asterisks' do
70
+ expect(processed_data).to include(field: '******')
71
+ end
72
+ end
73
+
74
+ context 'when field is number' do
75
+ let(:value) { 42 }
76
+
77
+ it 'masks number value' do
78
+ expect(processed_data).to include(field: '**')
79
+ end
80
+ end
81
+
82
+ context 'when field is boolean' do
83
+ let(:value) { true }
84
+
85
+ it 'masks value with one asterisk' do
86
+ expect(processed_data).to include(field: '*')
87
+ end
88
+ end
89
+
90
+ context 'when field is array' do
91
+ let(:value) { [1,2,3,4] }
92
+
93
+ it 'masks value with one asterisk' do
94
+ expect(processed_data).to include(field: '*')
95
+ end
96
+ end
97
+
98
+ context 'when field is hash' do
99
+ let(:value) { {data: {}} }
100
+
101
+ it 'masks value with one asterisk' do
102
+ expect(processed_data).to include(field: '*')
103
+ end
104
+ end
105
+
106
+ context 'when field is deeply nested' do
107
+ let(:depth) { 10 }
108
+ let(:data) { data_with_nested_field({field: 'secret'}, depth) }
109
+
110
+ it 'masks deeply nested field' do
111
+ expect(processed_data).to include_at_depth({field: '******'}, depth)
112
+ end
113
+ end
114
+ end
115
+
116
+ def data_with_nested_field(field, depth)
117
+ depth.times.inject(field) do |mem|
118
+ {}.merge(data: mem)
119
+ end
120
+ end
121
+
122
+ RSpec::Matchers.define :include_at_depth do |expected_hash, depth|
123
+ match do |actual|
124
+ nested_data = depth.times.inject(actual) do |mem|
125
+ mem[:data]
126
+ end
127
+
128
+ expect(nested_data).to include(expected_hash)
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,152 @@
1
+ require 'spec_helper'
2
+ require_relative '../../lib/logasm/preprocessors/whitelist'
3
+
4
+ describe Logasm::Preprocessors::Whitelist do
5
+ subject(:processed_data) { described_class.new(config).process(data) }
6
+
7
+ let(:config) { {pointers: pointers} }
8
+ let(:pointers) { [] }
9
+ let(:data) {{
10
+ field: 'secret',
11
+ data: {
12
+ field: 'secret'
13
+ },
14
+ array: [{field: 'secret'}]
15
+ }}
16
+
17
+ it 'masks all non-whitelisted fields' do
18
+ expect(processed_data).to eq({
19
+ field: '******',
20
+ data: {
21
+ field: '******'
22
+ },
23
+ array: [{field: '******'}]
24
+ })
25
+ end
26
+
27
+ context 'when pointer has trailing slash' do
28
+ let(:pointers) { ['/field/'] }
29
+
30
+ it 'throws exception' do
31
+ expect { processed_data }.to raise_exception(Logasm::Preprocessors::Whitelist::InvalidPointerFormatException)
32
+ end
33
+ end
34
+
35
+ context 'with whitelisted field' do
36
+ let(:pointers) { ['/field'] }
37
+
38
+ it 'includes the field' do
39
+ expect(processed_data).to eq({
40
+ field: 'secret',
41
+ data: {
42
+ field: '******'
43
+ },
44
+ array: [{field: '******'}]
45
+ })
46
+ end
47
+ end
48
+
49
+ context 'with whitelisted nested field' do
50
+ let(:pointers) { ['/data/field'] }
51
+
52
+ it 'includes nested field' do
53
+ expect(processed_data).to eq({
54
+ field: '******',
55
+ data: {
56
+ field: 'secret'
57
+ },
58
+ array: [{field: '******'}]
59
+ })
60
+ end
61
+ end
62
+
63
+ context 'with whitelisted array element field' do
64
+ let(:pointers) { ['/array/0/field'] }
65
+
66
+ it 'includes array element' do
67
+ expect(processed_data).to eq({
68
+ field: '******',
69
+ data: {
70
+ field: '******'
71
+ },
72
+ array: [{field: 'secret'}]
73
+ })
74
+ end
75
+ end
76
+
77
+ context 'with whitelisted array element' do
78
+ let(:pointers) { ['/array/0'] }
79
+
80
+ it 'masks array element' do
81
+ expect(processed_data).to include(array: [{field: '******'}])
82
+ end
83
+ end
84
+
85
+ context 'with whitelisted array' do
86
+ let(:pointers) { ['/array'] }
87
+
88
+ it 'masks array' do
89
+ expect(processed_data).to include(array: [{field: '******'}])
90
+ end
91
+ end
92
+
93
+ context 'with whitelisted hash' do
94
+ let(:pointers) { ['/data'] }
95
+
96
+ it 'masks hash' do
97
+ expect(processed_data).to include(data: {field: '******'})
98
+ end
99
+ end
100
+
101
+ context 'when boolean present' do
102
+ let(:data) { {bool: true} }
103
+
104
+ it 'masks it with single asteriks' do
105
+ expect(processed_data).to eq(bool: '*')
106
+ end
107
+ end
108
+
109
+ context 'when field has slash in the name' do
110
+ let(:data) {{
111
+ 'field_with_/' => 'secret'
112
+ }}
113
+ let(:pointers) { ['/field_with_~1'] }
114
+
115
+ it 'includes field' do
116
+ expect(processed_data).to include('field_with_/'=> 'secret')
117
+ end
118
+ end
119
+
120
+ context 'when field has tilde in the name' do
121
+ let(:data) {{
122
+ 'field_with_~' => 'secret'
123
+ }}
124
+ let(:pointers) { ['/field_with_~0'] }
125
+
126
+ it 'includes field' do
127
+ expect(processed_data).to include('field_with_~'=> 'secret')
128
+ end
129
+ end
130
+
131
+ context 'when field has slash in the name' do
132
+ let(:data) {{
133
+ 'field_with_/' => 'secret'
134
+ }}
135
+ let(:pointers) { ['/field_with_~1'] }
136
+
137
+ it 'includes field' do
138
+ expect(processed_data).to include('field_with_/'=> 'secret')
139
+ end
140
+ end
141
+
142
+ context 'when field has tilde in the name' do
143
+ let(:data) {{
144
+ 'field_with_~' => 'secret'
145
+ }}
146
+ let(:pointers) { ['/field_with_~0'] }
147
+
148
+ it 'includes field' do
149
+ expect(processed_data).to include('field_with_~'=> 'secret')
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,11 @@
1
+ RSpec::Matchers.define :implement_interface do |expected|
2
+ required_methods = expected.instance_methods(false)
3
+ match do |actual|
4
+ required_methods - actual.methods == []
5
+ end
6
+
7
+ failure_message do |actual|
8
+ missing_methods = required_methods - actual.methods
9
+ "Expected instance methods #{missing_methods.inspect} to be implemented"
10
+ end
11
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logasm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Salemove
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-30 00:00:00.000000000 Z
11
+ date: 2016-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inflecto
@@ -88,6 +88,9 @@ files:
88
88
  - lib/logasm/adapters/rabbitmq_adapter.rb
89
89
  - lib/logasm/adapters/stdout_adapter.rb
90
90
  - lib/logasm/null_logger.rb
91
+ - lib/logasm/preprocessors.rb
92
+ - lib/logasm/preprocessors/blacklist.rb
93
+ - lib/logasm/preprocessors/whitelist.rb
91
94
  - lib/logasm/utils.rb
92
95
  - logasm.gemspec
93
96
  - spec/adapters/logstash_adapter/formatter_spec.rb
@@ -95,8 +98,11 @@ files:
95
98
  - spec/adapters/rabbitmq_adapter_spec.rb
96
99
  - spec/adapters/stdout_adapter_spec.rb
97
100
  - spec/logasm_spec.rb
101
+ - spec/preprocessors/blacklist_spec.rb
102
+ - spec/preprocessors/whitelist_spec.rb
98
103
  - spec/spec_helper.rb
99
104
  - spec/support/freddy_mock.rb
105
+ - spec/support/implement_interface.rb
100
106
  - spec/utils_spec.rb
101
107
  homepage:
102
108
  licenses:
@@ -118,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
124
  version: '0'
119
125
  requirements: []
120
126
  rubyforge_project:
121
- rubygems_version: 2.4.5.1
127
+ rubygems_version: 2.4.8
122
128
  signing_key:
123
129
  specification_version: 4
124
130
  summary: What description said
@@ -128,6 +134,9 @@ test_files:
128
134
  - spec/adapters/rabbitmq_adapter_spec.rb
129
135
  - spec/adapters/stdout_adapter_spec.rb
130
136
  - spec/logasm_spec.rb
137
+ - spec/preprocessors/blacklist_spec.rb
138
+ - spec/preprocessors/whitelist_spec.rb
131
139
  - spec/spec_helper.rb
132
140
  - spec/support/freddy_mock.rb
141
+ - spec/support/implement_interface.rb
133
142
  - spec/utils_spec.rb