sensitive_data_filter 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6e5a914234df291b0800d47279ad6dfe4b9405f7
4
- data.tar.gz: 3450359e5f85f765783e4e142b6e2c4671a28765
3
+ metadata.gz: 70cedcd682fd58e3d8682c603bc8326c36e7b8ec
4
+ data.tar.gz: aee8dcf45f48b651e85507a6d0e0b02fe8fbb071
5
5
  SHA512:
6
- metadata.gz: 8f155d08c7c6904f1bb5b9a683a3c528fd6e1b61377bf4edd96cc0b02b147b4342957e9f36ae95a0e489ff4358e05f5b142f72492a5c20a9172c8d12ee3d424c
7
- data.tar.gz: 80262f7b96eb96c7564791459049c3e4d5a8a6508a85b1767004f6eaf0cb23e9c7709f7b52eb72d54cf4c331d5957951fb80fe106f4d2e672509afd7c5ffb697
6
+ metadata.gz: fc6618c4cdad98edb6899779d93b15a1cbfc5c8b650d2cce604f40b2c9178d24cf9000753ecf512ee82b99a17e580f8750cf3019ac6f0777532a417d32ffc8d9
7
+ data.tar.gz: 1f126f0eec67bc0c8bc74d7bb50e6121056a8113ce35454ba2fc7d1c36d6db74ede0736050515b96e53467d433844f6eef1a545554a01d58898fef0d117bf9eb
data/.rubocop.yml CHANGED
@@ -16,6 +16,9 @@ Style/SignalException:
16
16
  Style/SafeNavigation:
17
17
  Enabled: false
18
18
 
19
+ Style/RedundantFreeze:
20
+ Enabled: false
21
+
19
22
  Style/FileName:
20
23
  Exclude:
21
24
  - gemfiles/Gemfile.*.rb
data/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@ All notable changes to this project will be documented in this file.
3
3
  This project adheres to [Semantic Versioning](http://semver.org/).
4
4
  This changelog adheres to [Keep a CHANGELOG](http://keepachangelog.com/).
5
5
 
6
+ ## [0.2.0] - 2016-12-13
7
+ ### Added
8
+ - Occurrence exposes content type
9
+ - Support for different content types
10
+ - Native JSON parameter parsing
11
+ - Allows defining parameter parsers
12
+ - Scans and masks parameter keys
13
+ - Adds credit card brand validation
14
+
15
+ ### Changed
16
+ - Occurrence now exposes query and body params separately
17
+
18
+ ### Fixed
19
+ - Skips scanning of file uploads
20
+
6
21
  ## [0.1.0] - 2016-12-09
7
22
  ### Added
8
23
  - Whitelisting of matches
data/README.md CHANGED
@@ -44,24 +44,31 @@ SensitiveDataFilter.config do |config|
44
44
  # Report occurrence
45
45
  end
46
46
  config.whitelist pattern1, pattern2 # Allows specifying patterns to whitelist matches
47
+ config.register_parser('yaml', -> params { YAML.load params }, -> params { YAML.dump params })
47
48
  end
48
49
  ```
49
50
 
50
51
  An occurrence object has the following properties:
51
52
 
52
- * origin_ip: the IP address that originated the request
53
- * request_method: the HTTP method for the request (GET, POST, etc.)
54
- * url: the URL of the request
55
- * original_params: the parameters sent with the request
56
- * filtered_params: the parameters sent with the request, with sensitive data filtered
57
- * session: the session properties for the request
58
- * matches: the matched sensitive data
59
- * matches_count: the number of matches per data type, e.g. { 'CreditCard' => 1 }
53
+ * origin_ip: the IP address that originated the request
54
+ * request_method: the HTTP method for the request (GET, POST, etc.)
55
+ * url: the URL of the request
56
+ * content_type: the Content-Type of the request
57
+ * original_query_params: the query parameters sent with the request
58
+ * original_body_params: the body parameters sent with the request
59
+ * filtered_query_params: the query parameters sent with the request, with sensitive data filtered
60
+ * filtered_body_params: the body parameters sent with the request, with sensitive data filtered
61
+ * session: the session properties for the request
62
+ * matches: the matched sensitive data
63
+ * matches_count: the number of matches per data type, e.g. { 'CreditCard' => 1 }
60
64
 
61
65
  It also exposes `to_h` and `to_s` methods for hash and string representation respectively.
62
- Please note that these representations omit sensitive data, i.e. `original_params` and `matches` are not included.
66
+ Please note that these representations omit sensitive data,
67
+ i.e. `original_query_params`, `original_body_params` and `matches` are not included.
63
68
 
64
- #### Important Note
69
+ #### Important Notes
70
+
71
+ Body parameters will not be parsed if a parser for the request's content type is not defined.
65
72
 
66
73
  You might want to filter sensitive parameters (e.g: passwords).
67
74
  In Rails you can do something like:
@@ -69,9 +76,29 @@ In Rails you can do something like:
69
76
  ```ruby
70
77
  filters = Rails.application.config.filter_parameters
71
78
  filter = ActionDispatch::Http::ParameterFilter.new filters
72
- filter.filter @occurrence.filtered_params
79
+ filtered_query_params = filter.filter @occurrence.filtered_query_params
80
+ filtered_body_params = if @occurrence.filtered_body_params.is_a? Hash
81
+ filter.filter @occurrence.filtered_body_params
82
+ else
83
+ @occurrence.filtered_body_params
84
+ end
73
85
  ```
74
86
 
87
+ #### Whitelisting
88
+
89
+ A list of whitelisting patterns can be passed to `config.whitelist`.
90
+ Any sensitive data match which also matches any of these patterns will be ignored.
91
+
92
+ #### Parameter Parsing
93
+
94
+ Parsers for parameters encoded for a specific content type can be defined.
95
+ The arguments for `config.register_parser` are:
96
+ * a pattern to match the content type
97
+ * a parser for the parameters
98
+ * an unparser to convert parameters back to the encoded format
99
+
100
+ The parser and unparser must be objects that respond to `call` and accept the parameters as an argument (e.g. procs or lambdas).
101
+
75
102
  ## Development
76
103
 
77
104
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -3,6 +3,8 @@ source 'https://rubygems.org'
3
3
 
4
4
  # ruby-2.1 compatible gems
5
5
  gem 'rack', '~> 1.4'
6
+ gem 'activemodel', '>= 3', '< 5'
7
+ gem 'activesupport', '>= 3', '< 5'
6
8
 
7
9
  # Specify your gem's dependencies in sensitive_data_filter.gemspec
8
10
  gemspec path: '../'
@@ -3,6 +3,8 @@ source 'https://rubygems.org'
3
3
 
4
4
  # ruby-2.2 compatible gems
5
5
  gem 'rack', '~> 1.4'
6
+ gem 'activemodel', '>= 3', '< 5'
7
+ gem 'activesupport', '>= 3', '< 5'
6
8
 
7
9
  # Specify your gem's dependencies in sensitive_data_filter.gemspec
8
10
  gemspec path: '../'
@@ -44,5 +44,10 @@ module SensitiveDataFilter
44
44
  def whitelist_patterns
45
45
  @whitelist_patterns ||= []
46
46
  end
47
+
48
+ def register_parser(content_type, parser, unparser)
49
+ SensitiveDataFilter::Middleware::ParameterParser
50
+ .register_parser(content_type, parser, unparser)
51
+ end
47
52
  end
48
53
  end
@@ -2,13 +2,17 @@
2
2
  module SensitiveDataFilter
3
3
  module Mask
4
4
  module_function def mask(value)
5
+ return mask_array(value) if value.is_a? Array
6
+ return mask_hash(value) if value.is_a? Hash
5
7
  SensitiveDataFilter.enabled_types.inject(value) { |acc, elem| elem.mask acc }
6
8
  end
7
9
 
10
+ module_function def mask_array(array)
11
+ array.map { |element| mask(element) }
12
+ end
13
+
8
14
  module_function def mask_hash(hash)
9
- hash.map.with_object({}) { |(key, value), result|
10
- result[key] = mask(value)
11
- }
15
+ hash.map { |key, value| [mask(key), mask(value)] }.to_h
12
16
  end
13
17
  end
14
18
  end
@@ -4,8 +4,8 @@ module SensitiveDataFilter
4
4
  end
5
5
  end
6
6
 
7
+ require 'sensitive_data_filter/middleware/parameter_parser'
7
8
  require 'sensitive_data_filter/middleware/env_parser'
8
- require 'sensitive_data_filter/middleware/parameter_scanner'
9
9
  require 'sensitive_data_filter/middleware/occurrence'
10
10
  require 'sensitive_data_filter/middleware/env_filter'
11
11
  require 'sensitive_data_filter/middleware/filter'
@@ -9,8 +9,8 @@ module SensitiveDataFilter
9
9
  def initialize(env)
10
10
  @original_env_parser = EnvParser.new(env)
11
11
  @filtered_env_parser = @original_env_parser.copy
12
- @scanner = ParameterScanner.new(@original_env_parser)
13
- @filtered_env_parser.mask! if @scanner.sensitive_data?
12
+ @scan = build_scan
13
+ @filtered_env_parser.mask! if @scan.matches?
14
14
  @occurrence = build_occurrence
15
15
  end
16
16
 
@@ -25,8 +25,14 @@ module SensitiveDataFilter
25
25
  private
26
26
 
27
27
  def build_occurrence
28
- return nil unless @scanner.sensitive_data?
29
- Occurrence.new(@original_env_parser, @filtered_env_parser, @scanner.matches)
28
+ return nil unless @scan.matches?
29
+ Occurrence.new(@original_env_parser, @filtered_env_parser, @scan.matches)
30
+ end
31
+
32
+ def build_scan
33
+ SensitiveDataFilter::Scan.new(
34
+ [@original_env_parser.query_params, @original_env_parser.body_params]
35
+ )
30
36
  end
31
37
  end
32
38
  end
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
+ require 'forwardable'
3
+
2
4
  module SensitiveDataFilter
3
5
  module Middleware
4
6
  class EnvParser
7
+ QUERY_STRING = 'QUERY_STRING'.freeze
8
+ RACK_INPUT = 'rack.input'.freeze
9
+
5
10
  extend Forwardable
6
11
 
7
12
  attr_reader :env
@@ -9,6 +14,7 @@ module SensitiveDataFilter
9
14
  def initialize(env)
10
15
  @env = env
11
16
  @request = Rack::Request.new(@env)
17
+ @parameter_parser = ParameterParser.parser_for(@request.media_type)
12
18
  end
13
19
 
14
20
  def query_params
@@ -16,17 +22,18 @@ module SensitiveDataFilter
16
22
  end
17
23
 
18
24
  def body_params
25
+ return {} if file_upload?
19
26
  body = @request.body.read
20
27
  @request.body.rewind
21
- Rack::Utils.parse_query(body)
28
+ @parameter_parser.parse(body)
22
29
  end
23
30
 
24
31
  def query_params=(new_params)
25
- @env['QUERY_STRING'] = Rack::Utils.build_query(new_params)
32
+ @env[QUERY_STRING] = Rack::Utils.build_query(new_params)
26
33
  end
27
34
 
28
35
  def body_params=(new_params)
29
- @env['rack.input'] = StringIO.new Rack::Utils.build_query(new_params)
36
+ @env[RACK_INPUT] = StringIO.new @parameter_parser.unparse(new_params)
30
37
  end
31
38
 
32
39
  def copy
@@ -34,11 +41,17 @@ module SensitiveDataFilter
34
41
  end
35
42
 
36
43
  def mask!
37
- self.query_params = SensitiveDataFilter::Mask.mask_hash(query_params)
38
- self.body_params = SensitiveDataFilter::Mask.mask_hash(body_params)
44
+ self.query_params = SensitiveDataFilter::Mask.mask(query_params)
45
+ self.body_params = SensitiveDataFilter::Mask.mask(body_params)
39
46
  end
40
47
 
41
- def_delegators :@request, :ip, :request_method, :url, :params, :session
48
+ def_delegators :@request, :ip, :request_method, :url, :content_type, :session
49
+
50
+ private
51
+
52
+ def file_upload?
53
+ @request.media_type == 'multipart/form-data'
54
+ end
42
55
  end
43
56
  end
44
57
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'forwardable'
2
3
  require 'facets/string/titlecase'
3
4
 
4
5
  module SensitiveDataFilter
@@ -18,15 +19,23 @@ module SensitiveDataFilter
18
19
  @original_env_parser.ip
19
20
  end
20
21
 
21
- def original_params
22
- @original_env_parser.params
22
+ def original_query_params
23
+ @original_env_parser.query_params
23
24
  end
24
25
 
25
- def filtered_params
26
- @filtered_env_parser.params
26
+ def original_body_params
27
+ @original_env_parser.body_params
27
28
  end
28
29
 
29
- def_delegators :@original_env_parser, :request_method, :url, :session
30
+ def filtered_query_params
31
+ @filtered_env_parser.query_params
32
+ end
33
+
34
+ def filtered_body_params
35
+ @filtered_env_parser.body_params
36
+ end
37
+
38
+ def_delegators :@original_env_parser, :request_method, :url, :content_type, :session
30
39
 
31
40
  def matches_count
32
41
  @matches.map { |type, matches| [type, matches.count] }.to_h
@@ -34,12 +43,14 @@ module SensitiveDataFilter
34
43
 
35
44
  def to_h
36
45
  {
37
- origin_ip: origin_ip,
38
- request_method: request_method,
39
- url: url,
40
- filtered_params: filtered_params,
41
- session: session,
42
- matches_count: matches_count
46
+ origin_ip: origin_ip,
47
+ request_method: request_method,
48
+ url: url,
49
+ content_type: content_type,
50
+ filtered_query_params: filtered_query_params,
51
+ filtered_body_params: filtered_body_params,
52
+ session: session,
53
+ matches_count: matches_count
43
54
  }
44
55
  end
45
56
 
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SensitiveDataFilter
4
+ module Middleware
5
+ class ParameterParser
6
+ def self.register_parser(content_type, parse, unparse)
7
+ parsers.unshift new(content_type, parse, unparse)
8
+ end
9
+
10
+ def self.parsers
11
+ @parsers ||= DEFAULT_PARSERS.dup
12
+ end
13
+
14
+ def self.parser_for(content_type)
15
+ parsers.find { |parser| parser.can_parse? content_type } || NULL_PARSER
16
+ end
17
+
18
+ def initialize(content_type, parse, unparse)
19
+ @content_type = content_type
20
+ @parse = parse
21
+ @unparse = unparse
22
+ end
23
+
24
+ def can_parse?(content_type)
25
+ content_type.to_s.match @content_type
26
+ end
27
+
28
+ def parse(params)
29
+ @parse.call params
30
+ end
31
+
32
+ def unparse(params)
33
+ @unparse.call params
34
+ end
35
+
36
+ NULL_PARSER = new('', ->(params) { params }, ->(params) { params })
37
+
38
+ DEFAULT_PARSERS = [
39
+ new('urlencoded', # e.g.: 'application/x-www-form-urlencoded'
40
+ ->(params) { Rack::Utils.parse_query(params) },
41
+ ->(params) { Rack::Utils.build_query(params) }),
42
+ new('json', # e.g.: 'application/json'
43
+ ->(params) { JSON.parse(params) },
44
+ ->(params) { JSON.unparse(params) })
45
+ ].freeze
46
+ end
47
+ end
48
+ end
@@ -1,26 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
  require 'facets/kernel/present'
3
+ require 'facets/hash/collate'
3
4
 
4
5
  module SensitiveDataFilter
5
6
  class Scan
7
+ def self.scan(value)
8
+ return scan_array(value) if value.is_a? Array
9
+ return scan_hash(value) if value.is_a? Hash
10
+ SensitiveDataFilter.enabled_types.map.with_object({}) { |scanner, matches|
11
+ matches[scanner.name.split('::').last] = whitelist(scanner.scan(value))
12
+ }
13
+ end
14
+
15
+ def self.scan_array(array)
16
+ array.map { |element| scan(element) }.inject(:collate) || {}
17
+ end
18
+
19
+ def self.scan_hash(hash)
20
+ hash.map { |key, value| scan(key).collate(scan(value)) }.inject(:collate) || {}
21
+ end
22
+
23
+ def self.whitelist(matches)
24
+ matches.reject { |match| SensitiveDataFilter.whitelisted? match }
25
+ end
26
+
6
27
  def initialize(value)
7
28
  @value = value
8
29
  end
9
30
 
10
31
  def matches
11
- @matches ||= SensitiveDataFilter.enabled_types.map.with_object({}) { |scanner, matches|
12
- matches[scanner.name.split('::').last] = whitelist scanner.scan(@value)
13
- }
32
+ @matches ||= self.class.scan(@value)
14
33
  end
15
34
 
16
35
  def matches?
17
36
  matches.values.any?(&:present?)
18
37
  end
19
-
20
- private
21
-
22
- def whitelist(matches)
23
- matches.reject { |match| SensitiveDataFilter.whitelisted? match }
24
- end
25
38
  end
26
39
  end
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'credit_card_validations'
3
+
2
4
  module SensitiveDataFilter
3
5
  module Types
4
6
  module CreditCard
@@ -21,7 +23,7 @@ module SensitiveDataFilter
21
23
  module_function def valid?(number)
22
24
  return false unless number.is_a? String
23
25
  return false unless number.match CARD
24
- Luhn.new(number.gsub(SEPARATORS, '')).valid?
26
+ CreditCardValidations::Detector.new(number.gsub(SEPARATORS, '')).brand.present?
25
27
  end
26
28
 
27
29
  module_function def scan(value)
@@ -33,40 +35,6 @@ module SensitiveDataFilter
33
35
  return value unless value.is_a? String
34
36
  scan(value).inject(value) { |acc, elem| acc.gsub(elem, FILTERED) }
35
37
  end
36
-
37
- # Adapted from https://github.com/rolfb/luhn-ruby/blob/master/lib/luhn.rb
38
- class Luhn
39
- def initialize(number)
40
- @number = number
41
- end
42
-
43
- def valid?
44
- numbers = split_digits(@number)
45
- numbers.last == checksum(numbers[0..-2].join)
46
- end
47
-
48
- private
49
-
50
- def checksum(number)
51
- products = luhn_doubled(number)
52
- sum = products.inject(0) { |acc, elem| acc + sum_of(elem) }
53
- checksum = 10 - (sum % 10)
54
- checksum == 10 ? 0 : checksum
55
- end
56
-
57
- def luhn_doubled(number)
58
- numbers = split_digits(number).reverse
59
- numbers.map.with_index { |n, i| i.even? ? n * 2 : n * 1 }.reverse
60
- end
61
-
62
- def sum_of(number)
63
- split_digits(number).inject(:+)
64
- end
65
-
66
- def split_digits(number)
67
- number.to_s.split(//).map(&:to_i)
68
- end
69
- end
70
38
  end
71
39
  end
72
40
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module SensitiveDataFilter
3
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'
4
4
  end
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
25
25
 
26
26
  spec.add_dependency 'rack', '>= 1.4'
27
27
  spec.add_dependency 'facets', '~> 3.1'
28
+ spec.add_dependency 'credit_card_validations', '~> 3.2'
28
29
 
29
30
  spec.add_development_dependency 'bundler', '~> 1.13'
30
31
  spec.add_development_dependency 'rake', '~> 10.0'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sensitive_data_filter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Berardi
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2016-12-09 00:00:00.000000000 Z
12
+ date: 2016-12-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -39,6 +39,20 @@ dependencies:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
41
  version: '3.1'
42
+ - !ruby/object:Gem::Dependency
43
+ name: credit_card_validations
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '3.2'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '3.2'
42
56
  - !ruby/object:Gem::Dependency
43
57
  name: bundler
44
58
  requirement: !ruby/object:Gem::Requirement
@@ -182,7 +196,7 @@ files:
182
196
  - lib/sensitive_data_filter/middleware/env_parser.rb
183
197
  - lib/sensitive_data_filter/middleware/filter.rb
184
198
  - lib/sensitive_data_filter/middleware/occurrence.rb
185
- - lib/sensitive_data_filter/middleware/parameter_scanner.rb
199
+ - lib/sensitive_data_filter/middleware/parameter_parser.rb
186
200
  - lib/sensitive_data_filter/scan.rb
187
201
  - lib/sensitive_data_filter/types.rb
188
202
  - lib/sensitive_data_filter/types/credit_card.rb
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
- require 'facets/hash/collate'
3
-
4
- module SensitiveDataFilter
5
- module Middleware
6
- class ParameterScanner
7
- def initialize(env_parser)
8
- @env_parser = env_parser
9
- @params = @env_parser.query_params.values + @env_parser.body_params.values
10
- @scans = @params.map { |value| SensitiveDataFilter::Scan.new(value) }
11
- end
12
-
13
- def matches
14
- @scans.map(&:matches).inject(:collate)
15
- end
16
-
17
- def sensitive_data?
18
- @scans.any?(&:matches?)
19
- end
20
- end
21
- end
22
- end