logstash-filter-fingerprint 0.1.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 ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ M2IxMjQ1ZjcyMWI0NzE2ZmExYzg1ZGY2MTA5YjE0YjkyYzgxN2MxZg==
5
+ data.tar.gz: !binary |-
6
+ OWZiMTA4NGY1NTViY2ExZjNhNDJkZTMxYzc0OTcyZmRhZTU0MGJmNQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NmZkZjJhMzg2MTAzOTAxMDQ5NzZiNDdhZjEyOTA5YTE3YjBmOGI2Y2NiZjQz
10
+ N2FmMWM3OGMwNDYwNjg5NzQ4NmU5YjFjODY2ZmIzMTUzNWRmOWY2NDEwYTZl
11
+ YWI0MWQ5NWVmOWM3ODY5YmNjNmFlZGI5MzM5MjhmNDEyMmM2YmM=
12
+ data.tar.gz: !binary |-
13
+ OGYwNGI2NGRmMjAyZmM3NGJiY2E3NzAzYTQ5N2JmZDY0MmNhNTVhMWU3ODNj
14
+ NTZjNGUxY2QyYWMwMjcxOTJkMThjZjEwMDJiNWFjMDdiMmEzNWVmYjMzN2M4
15
+ ZWY4MDMzNDAxMjYzZjQ5NTdiYmJjODcxMjlkNGU3YWJlMzBkOGI=
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ .bundle
4
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+ gem 'rake'
3
+ gem 'gem_publisher'
4
+ gem 'archive-tar-minitar'
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2012-2014 Elasticsearch <http://www.elasticsearch.org>
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ @files=[]
2
+
3
+ task :default do
4
+ system("rake -T")
5
+ end
6
+
@@ -0,0 +1,122 @@
1
+ # encoding: utf-8
2
+ require "logstash/filters/base"
3
+ require "logstash/namespace"
4
+
5
+ # Fingerprint fields using by replacing values with a consistent hash.
6
+ class LogStash::Filters::Fingerprint < LogStash::Filters::Base
7
+ config_name "fingerprint"
8
+ milestone 1
9
+
10
+ # Source field(s)
11
+ config :source, :validate => :array, :default => 'message'
12
+
13
+ # Target field.
14
+ # will overwrite current value of a field if it exists.
15
+ config :target, :validate => :string, :default => 'fingerprint'
16
+
17
+ # When used with IPV4_NETWORK method fill in the subnet prefix length
18
+ # Not required for MURMUR3 or UUID methods
19
+ # With other methods fill in the HMAC key
20
+ config :key, :validate => :string
21
+
22
+ # Fingerprint method
23
+ config :method, :validate => ['SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', "MURMUR3", "IPV4_NETWORK", "UUID", "PUNCTUATION"], :required => true, :default => 'SHA1'
24
+
25
+ # When set to true, we concatenate the values of all fields into 1 string like the old checksum filter.
26
+ config :concatenate_sources, :validate => :boolean, :default => false
27
+
28
+ def register
29
+ # require any library and set the anonymize function
30
+ case @method
31
+ when "IPV4_NETWORK"
32
+ require 'ipaddr'
33
+ @logger.error("Key value is empty. please fill in a subnet prefix length") if @key.nil?
34
+ class << self; alias_method :anonymize, :anonymize_ipv4_network; end
35
+ when "MURMUR3"
36
+ require "murmurhash3"
37
+ class << self; alias_method :anonymize, :anonymize_murmur3; end
38
+ when "UUID"
39
+ require "securerandom"
40
+ when "PUNCTUATION"
41
+ # nothing required
42
+ else
43
+ require 'openssl'
44
+ @logger.error("Key value is empty. Please fill in an encryption key") if @key.nil?
45
+ class << self; alias_method :anonymize, :anonymize_openssl; end
46
+ end
47
+ end # def register
48
+
49
+ public
50
+ def filter(event)
51
+ return unless filter?(event)
52
+ case @method
53
+ when "UUID"
54
+ event[@target] = SecureRandom.uuid
55
+ when "PUNCTUATION"
56
+ @source.sort.each do |field|
57
+ next unless event.include?(field)
58
+ event[@target] = event[field].tr('A-Za-z0-9 \t','')
59
+ end
60
+ else
61
+ if @concatenate_sources
62
+ to_string = ''
63
+ @source.sort.each do |k|
64
+ @logger.debug("Adding key to string")
65
+ to_string << "|#{k}|#{event[k]}"
66
+ end
67
+ to_string << "|"
68
+ @logger.debug("String built", :to_checksum => to_string)
69
+ event[@target] = anonymize(to_string)
70
+ else
71
+ @source.each do |field|
72
+ next unless event.include?(field)
73
+ if event[field].is_a?(Array)
74
+ event[@target] = event[field].collect { |v| anonymize(v) }
75
+ else
76
+ event[@target] = anonymize(event[field])
77
+ end
78
+ end # @source.each
79
+ end # concatenate_sources
80
+
81
+ end # casse @method
82
+ end # def filter
83
+
84
+ private
85
+ def anonymize_ipv4_network(ip_string)
86
+ # in JRuby 1.7.11 outputs as US-ASCII
87
+ IPAddr.new(ip_string).mask(@key.to_i).to_s.force_encoding(Encoding::UTF_8)
88
+ end
89
+
90
+ def anonymize_openssl(data)
91
+ digest = encryption_algorithm()
92
+ # in JRuby 1.7.11 outputs as ASCII-8BIT
93
+ OpenSSL::HMAC.hexdigest(digest, @key, data.to_s).force_encoding(Encoding::UTF_8)
94
+ end
95
+
96
+ def anonymize_murmur3(value)
97
+ case value
98
+ when Fixnum
99
+ MurmurHash3::V32.int_hash(value)
100
+ else
101
+ MurmurHash3::V32.str_hash(value.to_s)
102
+ end
103
+ end
104
+
105
+ def encryption_algorithm
106
+ case @method
107
+ when 'SHA1'
108
+ return OpenSSL::Digest::SHA1.new
109
+ when 'SHA256'
110
+ return OpenSSL::Digest::SHA256.new
111
+ when 'SHA384'
112
+ return OpenSSL::Digest::SHA384.new
113
+ when 'SHA512'
114
+ return OpenSSL::Digest::SHA512.new
115
+ when 'MD5'
116
+ return OpenSSL::Digest::MD5.new
117
+ else
118
+ @logger.error("Unknown algorithm")
119
+ end
120
+ end
121
+
122
+ end # class LogStash::Filters::Anonymize
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-filter-fingerprint'
4
+ s.version = '0.1.0'
5
+ s.licenses = ['Apache License (2.0)']
6
+ s.summary = "Fingerprint fields using by replacing values with a consistent hash."
7
+ s.description = "Fingerprint fields using by replacing values with a consistent hash."
8
+ s.authors = ["Elasticsearch"]
9
+ s.email = 'richard.pijnenburg@elasticsearch.com'
10
+ s.homepage = "http://logstash.net/"
11
+ s.require_paths = ["lib"]
12
+
13
+ # Files
14
+ s.files = `git ls-files`.split($\)+::Dir.glob('vendor/*')
15
+
16
+ # Tests
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+
19
+ # Special flag to let us know this is actually a logstash plugin
20
+ s.metadata = { "logstash_plugin" => "true", "group" => "filter" }
21
+
22
+ # Gem dependencies
23
+ s.add_runtime_dependency 'logstash', '>= 1.4.0', '< 2.0.0'
24
+ s.add_runtime_dependency "murmurhash3" #(MIT license)
25
+ end
26
+
@@ -0,0 +1,9 @@
1
+ require "gem_publisher"
2
+
3
+ desc "Publish gem to RubyGems.org"
4
+ task :publish_gem do |t|
5
+ gem_file = Dir.glob(File.expand_path('../*.gemspec',File.dirname(__FILE__))).first
6
+ gem = GemPublisher.publish_if_updated(gem_file, :rubygems)
7
+ puts "Published #{gem}" if gem
8
+ end
9
+
@@ -0,0 +1,169 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "digest/sha1"
4
+
5
+ def vendor(*args)
6
+ return File.join("vendor", *args)
7
+ end
8
+
9
+ directory "vendor/" => ["vendor"] do |task, args|
10
+ mkdir task.name
11
+ end
12
+
13
+ def fetch(url, sha1, output)
14
+
15
+ puts "Downloading #{url}"
16
+ actual_sha1 = download(url, output)
17
+
18
+ if actual_sha1 != sha1
19
+ fail "SHA1 does not match (expected '#{sha1}' but got '#{actual_sha1}')"
20
+ end
21
+ end # def fetch
22
+
23
+ def file_fetch(url, sha1)
24
+ filename = File.basename( URI(url).path )
25
+ output = "vendor/#{filename}"
26
+ task output => [ "vendor/" ] do
27
+ begin
28
+ actual_sha1 = file_sha1(output)
29
+ if actual_sha1 != sha1
30
+ fetch(url, sha1, output)
31
+ end
32
+ rescue Errno::ENOENT
33
+ fetch(url, sha1, output)
34
+ end
35
+ end.invoke
36
+
37
+ return output
38
+ end
39
+
40
+ def file_sha1(path)
41
+ digest = Digest::SHA1.new
42
+ fd = File.new(path, "r")
43
+ while true
44
+ begin
45
+ digest << fd.sysread(16384)
46
+ rescue EOFError
47
+ break
48
+ end
49
+ end
50
+ return digest.hexdigest
51
+ ensure
52
+ fd.close if fd
53
+ end
54
+
55
+ def download(url, output)
56
+ uri = URI(url)
57
+ digest = Digest::SHA1.new
58
+ tmp = "#{output}.tmp"
59
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => (uri.scheme == "https")) do |http|
60
+ request = Net::HTTP::Get.new(uri.path)
61
+ http.request(request) do |response|
62
+ fail "HTTP fetch failed for #{url}. #{response}" if [200, 301].include?(response.code)
63
+ size = (response["content-length"].to_i || -1).to_f
64
+ count = 0
65
+ File.open(tmp, "w") do |fd|
66
+ response.read_body do |chunk|
67
+ fd.write(chunk)
68
+ digest << chunk
69
+ if size > 0 && $stdout.tty?
70
+ count += chunk.bytesize
71
+ $stdout.write(sprintf("\r%0.2f%%", count/size * 100))
72
+ end
73
+ end
74
+ end
75
+ $stdout.write("\r \r") if $stdout.tty?
76
+ end
77
+ end
78
+
79
+ File.rename(tmp, output)
80
+
81
+ return digest.hexdigest
82
+ rescue SocketError => e
83
+ puts "Failure while downloading #{url}: #{e}"
84
+ raise
85
+ ensure
86
+ File.unlink(tmp) if File.exist?(tmp)
87
+ end # def download
88
+
89
+ def untar(tarball, &block)
90
+ require "archive/tar/minitar"
91
+ tgz = Zlib::GzipReader.new(File.open(tarball))
92
+ # Pull out typesdb
93
+ tar = Archive::Tar::Minitar::Input.open(tgz)
94
+ tar.each do |entry|
95
+ path = block.call(entry)
96
+ next if path.nil?
97
+ parent = File.dirname(path)
98
+
99
+ mkdir_p parent unless File.directory?(parent)
100
+
101
+ # Skip this file if the output file is the same size
102
+ if entry.directory?
103
+ mkdir path unless File.directory?(path)
104
+ else
105
+ entry_mode = entry.instance_eval { @mode } & 0777
106
+ if File.exists?(path)
107
+ stat = File.stat(path)
108
+ # TODO(sissel): Submit a patch to archive-tar-minitar upstream to
109
+ # expose headers in the entry.
110
+ entry_size = entry.instance_eval { @size }
111
+ # If file sizes are same, skip writing.
112
+ next if stat.size == entry_size && (stat.mode & 0777) == entry_mode
113
+ end
114
+ puts "Extracting #{entry.full_name} from #{tarball} #{entry_mode.to_s(8)}"
115
+ File.open(path, "w") do |fd|
116
+ # eof? check lets us skip empty files. Necessary because the API provided by
117
+ # Archive::Tar::Minitar::Reader::EntryStream only mostly acts like an
118
+ # IO object. Something about empty files in this EntryStream causes
119
+ # IO.copy_stream to throw "can't convert nil into String" on JRuby
120
+ # TODO(sissel): File a bug about this.
121
+ while !entry.eof?
122
+ chunk = entry.read(16384)
123
+ fd.write(chunk)
124
+ end
125
+ #IO.copy_stream(entry, fd)
126
+ end
127
+ File.chmod(entry_mode, path)
128
+ end
129
+ end
130
+ tar.close
131
+ File.unlink(tarball) if File.file?(tarball)
132
+ end # def untar
133
+
134
+ def ungz(file)
135
+
136
+ outpath = file.gsub('.gz', '')
137
+ tgz = Zlib::GzipReader.new(File.open(file))
138
+ begin
139
+ File.open(outpath, "w") do |out|
140
+ IO::copy_stream(tgz, out)
141
+ end
142
+ File.unlink(file)
143
+ rescue
144
+ File.unlink(outpath) if File.file?(outpath)
145
+ raise
146
+ end
147
+ tgz.close
148
+ end
149
+
150
+ desc "Process any vendor files required for this plugin"
151
+ task "vendor" do |task, args|
152
+
153
+ @files.each do |file|
154
+ download = file_fetch(file['url'], file['sha1'])
155
+ if download =~ /.tar.gz/
156
+ prefix = download.gsub('.tar.gz', '').gsub('vendor/', '')
157
+ untar(download) do |entry|
158
+ if !file['files'].nil?
159
+ next unless file['files'].include?(entry.full_name.gsub(prefix, ''))
160
+ out = entry.full_name.split("/").last
161
+ end
162
+ File.join('vendor', out)
163
+ end
164
+ elsif download =~ /.gz/
165
+ ungz(download)
166
+ end
167
+ end
168
+
169
+ end
@@ -0,0 +1,200 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+ require "logstash/filters/fingerprint"
4
+
5
+ describe LogStash::Filters::Fingerprint do
6
+
7
+ describe "fingerprint ipaddress with IPV4_NETWORK method" do
8
+ config <<-CONFIG
9
+ filter {
10
+ fingerprint {
11
+ source => ["clientip"]
12
+ method => "IPV4_NETWORK"
13
+ key => 24
14
+ }
15
+ }
16
+ CONFIG
17
+
18
+ sample("clientip" => "233.255.13.44") do
19
+ insist { subject["fingerprint"] } == "233.255.13.0"
20
+ end
21
+ end
22
+
23
+ describe "fingerprint string with MURMUR3 method" do
24
+ config <<-CONFIG
25
+ filter {
26
+ fingerprint {
27
+ source => ["clientip"]
28
+ method => "MURMUR3"
29
+ }
30
+ }
31
+ CONFIG
32
+
33
+ sample("clientip" => "123.52.122.33") do
34
+ insist { subject["fingerprint"] } == 1541804874
35
+ end
36
+ end
37
+
38
+ describe "fingerprint string with SHA1 alogrithm" do
39
+ config <<-CONFIG
40
+ filter {
41
+ fingerprint {
42
+ source => ["clientip"]
43
+ key => "longencryptionkey"
44
+ method => 'SHA1'
45
+ }
46
+ }
47
+ CONFIG
48
+
49
+ sample("clientip" => "123.123.123.123") do
50
+ insist { subject["fingerprint"] } == "fdc60acc4773dc5ac569ffb78fcb93c9630797f4"
51
+ end
52
+ end
53
+
54
+ describe "fingerprint string with SHA256 alogrithm" do
55
+ config <<-CONFIG
56
+ filter {
57
+ fingerprint {
58
+ source => ["clientip"]
59
+ key => "longencryptionkey"
60
+ method => 'SHA256'
61
+ }
62
+ }
63
+ CONFIG
64
+
65
+ sample("clientip" => "123.123.123.123") do
66
+ insist { subject["fingerprint"] } == "345bec3eff242d53b568916c2610b3e393d885d6b96d643f38494fd74bf4a9ca"
67
+ end
68
+ end
69
+
70
+ describe "fingerprint string with SHA384 alogrithm" do
71
+ config <<-CONFIG
72
+ filter {
73
+ fingerprint {
74
+ source => ["clientip"]
75
+ key => "longencryptionkey"
76
+ method => 'SHA384'
77
+ }
78
+ }
79
+ CONFIG
80
+
81
+ sample("clientip" => "123.123.123.123") do
82
+ insist { subject["fingerprint"] } == "22d4c0e8c4fbcdc4887d2038fca7650f0e2e0e2457ff41c06eb2a980dded6749561c814fe182aff93e2538d18593947a"
83
+ end
84
+ end
85
+
86
+ describe "fingerprint string with SHA512 alogrithm" do
87
+ config <<-CONFIG
88
+ filter {
89
+ fingerprint {
90
+ source => ["clientip"]
91
+ key => "longencryptionkey"
92
+ method => 'SHA512'
93
+ }
94
+ }
95
+ CONFIG
96
+
97
+ sample("clientip" => "123.123.123.123") do
98
+ insist { subject["fingerprint"] } == "11c19b326936c08d6c50a3c847d883e5a1362e6a64dd55201a25f2c1ac1b673f7d8bf15b8f112a4978276d573275e3b14166e17246f670c2a539401c5bfdace8"
99
+ end
100
+ end
101
+
102
+ describe "fingerprint string with MD5 alogrithm" do
103
+ config <<-CONFIG
104
+ filter {
105
+ fingerprint {
106
+ source => ["clientip"]
107
+ key => "longencryptionkey"
108
+ method => 'MD5'
109
+ }
110
+ }
111
+ CONFIG
112
+
113
+ sample("clientip" => "123.123.123.123") do
114
+ insist { subject["fingerprint"] } == "9336c879e305c9604a3843fc3e75948f"
115
+ end
116
+ end
117
+
118
+ describe "Test field with multiple values" do
119
+ config <<-CONFIG
120
+ filter {
121
+ fingerprint {
122
+ source => ["clientip"]
123
+ key => "longencryptionkey"
124
+ method => 'MD5'
125
+ }
126
+ }
127
+ CONFIG
128
+
129
+ sample("clientip" => [ "123.123.123.123", "223.223.223.223" ]) do
130
+ insist { subject["fingerprint"]} == [ "9336c879e305c9604a3843fc3e75948f", "7a6c66b8d3f42a7d650e3354af508df3" ]
131
+ end
132
+ end
133
+
134
+ describe "Concatenate multiple values into 1" do
135
+ config <<-CONFIG
136
+ filter {
137
+ fingerprint {
138
+ source => ['field1', 'field2']
139
+ key => "longencryptionkey"
140
+ method => 'MD5'
141
+ }
142
+ }
143
+ CONFIG
144
+
145
+ sample("field1" => "test1", "field2" => "test2") do
146
+ insist { subject["fingerprint"]} == "872da745e45192c2a1d4bf7c1ff8a370"
147
+ end
148
+ end
149
+
150
+ describe "PUNCTUATION method" do
151
+ config <<-CONFIG
152
+ filter {
153
+ fingerprint {
154
+ source => 'field1'
155
+ method => 'PUNCTUATION'
156
+ }
157
+ }
158
+ CONFIG
159
+
160
+ sample("field1" => "PHP Warning: json_encode() [<a href='function.json-encode'>function.json-encode</a>]: Invalid UTF-8 sequence in argument in /var/www/htdocs/test.php on line 233") do
161
+ insist { subject["fingerprint"] } == ":_()[<='.-'>.-</>]:-////."
162
+ end
163
+ end
164
+
165
+ context 'Timestamps' do
166
+ epoch_time = Time.at(0).gmtime
167
+
168
+ describe 'OpenSSL Fingerprinting' do
169
+ config <<-CONFIG
170
+ filter {
171
+ fingerprint {
172
+ source => ['@timestamp']
173
+ key => '0123'
174
+ method => 'SHA1'
175
+ }
176
+ }
177
+ CONFIG
178
+
179
+ sample("@timestamp" => epoch_time) do
180
+ insist { subject["fingerprint"] } == '1d5379ec92d86a67cfc642d55aa050ca312d3b9a'
181
+ end
182
+ end
183
+
184
+ describe 'MURMUR3 Fingerprinting' do
185
+ config <<-CONFIG
186
+ filter {
187
+ fingerprint {
188
+ source => ['@timestamp']
189
+ method => 'MURMUR3'
190
+ }
191
+ }
192
+ CONFIG
193
+
194
+ sample("@timestamp" => epoch_time) do
195
+ insist { subject["fingerprint"] } == 743372282
196
+ end
197
+ end
198
+ end
199
+
200
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-filter-fingerprint
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Elasticsearch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logstash
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.4.0
20
+ - - <
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.4.0
30
+ - - <
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: murmurhash3
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ description: Fingerprint fields using by replacing values with a consistent hash.
48
+ email: richard.pijnenburg@elasticsearch.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE
56
+ - Rakefile
57
+ - lib/logstash/filters/fingerprint.rb
58
+ - logstash-filter-fingerprint.gemspec
59
+ - rakelib/publish.rake
60
+ - rakelib/vendor.rake
61
+ - spec/filters/fingerprint_spec.rb
62
+ homepage: http://logstash.net/
63
+ licenses:
64
+ - Apache License (2.0)
65
+ metadata:
66
+ logstash_plugin: 'true'
67
+ group: filter
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubyforge_project:
84
+ rubygems_version: 2.4.1
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: Fingerprint fields using by replacing values with a consistent hash.
88
+ test_files:
89
+ - spec/filters/fingerprint_spec.rb