sensitive_data_filter 0.1.0 → 0.2.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: 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