fluent-plugin-pan-anonymizer 0.0.1
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 +7 -0
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/LICENSE +13 -0
- data/README.md +67 -0
- data/Rakefile +9 -0
- data/fluent-plugin-pan-anonymizer.gemspec +25 -0
- data/lib/fluent/plugin/filter_pan_anonymizer.rb +44 -0
- data/lib/fluent/plugin/pan/masker.rb +71 -0
- data/test/helper.rb +7 -0
- data/test/plugin/test_filter_pan_anonymizer.rb +308 -0
- data/test/plugin/test_pan_masker.rb +184 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e2a2096566adc99fe1643de5c70647c02ee9c8fd
|
4
|
+
data.tar.gz: 6298e53f30b28d786a0a0e63c016d50f07ffe332
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '09d07c442d389a04d1c312fee7b4617bee74f0152a901bbf8bc0fef214d75d726ca0825c9e0087e17e09dc1a4e91b0c120a2c91ad63163773d4bce4001568316'
|
7
|
+
data.tar.gz: 16f0a355f3156c24011fe90bce23036dbd3a9a87f1cc616d14a9b1f7e83beeba79e613f1b7270c019ee7e78c2c77d2e18702b804c075e824fa8b5c079bec95b7
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright (c) 2018- Kanmu, Inc.
|
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/README.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# fluent-plugin-pan-anonymizer
|
2
|
+
|
3
|
+
A Fluent filter plugin to anonymize records which have PAN (Primary Account Number = Credit card number). The plugin validates PAN using [Luhn algorithm](https://en.wikipedia.org/wiki/Luhn_algorithm) after matching.
|
4
|
+
|
5
|
+
Inspired by [fluent-plugin-anonymizer](https://github.com/y-ken/fluent-plugin-anonymizer).
|
6
|
+
|
7
|
+
# Requirements
|
8
|
+
|
9
|
+
- fluentd: v0.14.x or later
|
10
|
+
- Ruby: 2.4 or later
|
11
|
+
|
12
|
+
# Installation
|
13
|
+
|
14
|
+
```
|
15
|
+
gem install fluent-plugin-pan-anonymizer
|
16
|
+
```
|
17
|
+
|
18
|
+
# Configuration
|
19
|
+
|
20
|
+
NOTE: Card numbers in the example don't exist in the world.
|
21
|
+
|
22
|
+
```
|
23
|
+
<source>
|
24
|
+
@type dummy
|
25
|
+
tag dummy
|
26
|
+
dummy [
|
27
|
+
{"time": 12345678901234567, "subject": "xxxxxx", "user_inquiry": "hi, my card number is 4019249331712145 !"},
|
28
|
+
{"time": 12345678901234568, "subject": "xxxxxx", "user_inquiry": "hello inquiry code is 4567890123456789"},
|
29
|
+
{"time": 12345678901234569, "subject": "I am 4019 2493 3171 2145", "user_inquiry": "4019-2493-3171-2145 is my number"},
|
30
|
+
{"time": 14019249331712145, "subject": "ユーザーです", "user_inquiry": "4019249331712145 のカードを使っています"}
|
31
|
+
]
|
32
|
+
</source>
|
33
|
+
|
34
|
+
<filter **>
|
35
|
+
@type pan_anonymizer
|
36
|
+
ignore_keys time
|
37
|
+
<pan>
|
38
|
+
formats /4\d{15}/, /4[0-9]{15}/
|
39
|
+
checksum_algorithm luhn
|
40
|
+
mask 9999999999999999
|
41
|
+
</pan>
|
42
|
+
<pan>
|
43
|
+
formats /4\d{3}-\d{4}-\d{4}-\d{4}/, /4\d{3}\s*\d{4}\s*\d{4}\s*\d{4}/
|
44
|
+
checksum_algorithm luhn
|
45
|
+
mask xxxx-xxxx-xxxx-xxxx
|
46
|
+
</pan>
|
47
|
+
</filter>
|
48
|
+
|
49
|
+
<match **>
|
50
|
+
@type stdout
|
51
|
+
</match>
|
52
|
+
```
|
53
|
+
|
54
|
+
## The result of the example given above
|
55
|
+
|
56
|
+
```
|
57
|
+
2018-11-13 22:01:35.074963000 +0900 dummy: {"time":12345678901234567,"subject":"xxxxxx","user_inquiry":"hi, my card number is 9999999999999999 !"}
|
58
|
+
2018-11-13 22:01:36.001053000 +0900 dummy: {"time":12345678901234568,"subject":"xxxxxx","user_inquiry":"hello inquiry code is 4567890123456789"}
|
59
|
+
2018-11-13 22:01:37.021032000 +0900 dummy: {"time":12345678901234569,"subject":"I am xxxx-xxxx-xxxx-xxxx","user_inquiry":"xxxx-xxxx-xxxx-xxxx is my number"}
|
60
|
+
2018-11-13 22:01:38.050578000 +0900 dummy: {"time":14019249331712145,"subject":"ユーザーです","user_inquiry":"9999999999999999 のカードを使っています"}
|
61
|
+
```
|
62
|
+
|
63
|
+
Card numbers were masked with given configuration except `time` key and `4567890123456789` in "hello inquiry code is 4567890123456789". `4567890123456789` is not a valid card number.
|
64
|
+
|
65
|
+
# License
|
66
|
+
|
67
|
+
Apache License, Version 2.0
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "fluent-plugin-pan-anonymizer"
|
7
|
+
spec.version = "0.0.1"
|
8
|
+
spec.authors = ["Hiroaki Sano"]
|
9
|
+
spec.email = ["hiroaki.sano.9stories@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{Fluentd filter plugin to anonymize credit card numbers.}
|
12
|
+
spec.homepage = "https://github.com/kanmu/fluent-plugin-pan-anonymizer"
|
13
|
+
spec.license = "Apache License, Version 2.0"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split("\n")
|
16
|
+
spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "test-unit"
|
23
|
+
|
24
|
+
spec.add_runtime_dependency "fluentd", ">= 0.14.0"
|
25
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'fluent/plugin/filter'
|
2
|
+
require 'fluent/plugin/pan/masker'
|
3
|
+
|
4
|
+
module Fluent::Plugin
|
5
|
+
class PANAnonymizerFilter < Filter
|
6
|
+
Fluent::Plugin.register_filter("pan_anonymizer", self)
|
7
|
+
|
8
|
+
config_section :pan, param_name: :pan_configs, required: true, multi: true do
|
9
|
+
config_param :formats, :array, value_type: :regexp, default: []
|
10
|
+
config_param :checksum_algorithm, :enum, list: Fluent::PAN::Masker::CHECKSUM_FUNC.keys, default: :luhn
|
11
|
+
config_param :mask, :string, default: "****"
|
12
|
+
config_param :force, :bool, default: false
|
13
|
+
end
|
14
|
+
config_param :ignore_keys, :array, default: []
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure(conf)
|
21
|
+
super
|
22
|
+
|
23
|
+
@pan_masker = @pan_configs.map do |i|
|
24
|
+
i[:formats].map do |format|
|
25
|
+
Fluent::PAN::Masker.new(format, i[:checksum_algorithm], i[:mask], i[:force])
|
26
|
+
end
|
27
|
+
end.flatten
|
28
|
+
end
|
29
|
+
|
30
|
+
def filter(tag, time, record)
|
31
|
+
record.map do |key, value|
|
32
|
+
if @ignore_keys.include? key.to_s
|
33
|
+
[key, value]
|
34
|
+
else
|
35
|
+
_value = value
|
36
|
+
@pan_masker.each do |i|
|
37
|
+
_value = i.mask_if_found_pan(_value)
|
38
|
+
end
|
39
|
+
[key, _value]
|
40
|
+
end
|
41
|
+
end.to_h
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Fluent::PAN
|
2
|
+
class Masker
|
3
|
+
|
4
|
+
CHECKSUM_FUNC = {
|
5
|
+
luhn: ->(digits){
|
6
|
+
sum = 0
|
7
|
+
alt = false
|
8
|
+
digits.reverse.each do |i|
|
9
|
+
if alt
|
10
|
+
i *= 2
|
11
|
+
if i > 9
|
12
|
+
i -= 9
|
13
|
+
end
|
14
|
+
end
|
15
|
+
sum += i
|
16
|
+
alt = !alt
|
17
|
+
end
|
18
|
+
(sum % 10).zero?
|
19
|
+
},
|
20
|
+
none: ->digits{
|
21
|
+
# Do nothing. always satisfied.
|
22
|
+
true
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
def initialize(regexp, checksum_algorithm, mask, force=false)
|
27
|
+
@regexp = regexp
|
28
|
+
@mask = mask
|
29
|
+
@force = force
|
30
|
+
@checksum_func = CHECKSUM_FUNC[checksum_algorithm]
|
31
|
+
end
|
32
|
+
|
33
|
+
def mask_if_found_pan(orgval)
|
34
|
+
filtered = orgval.to_s.gsub(@regexp) do |match|
|
35
|
+
pan = match.split("").select { |i| i =~ /\d/ }.map { |j| j.to_i }
|
36
|
+
|
37
|
+
if valid?(pan)
|
38
|
+
match = @mask
|
39
|
+
else
|
40
|
+
match
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
retval = filtered
|
45
|
+
if orgval.is_a? Integer
|
46
|
+
if numerals_mask?
|
47
|
+
retval = filtered.to_i
|
48
|
+
else
|
49
|
+
if @force
|
50
|
+
retval = filtered
|
51
|
+
else
|
52
|
+
retval = orgval
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
retval
|
57
|
+
end
|
58
|
+
|
59
|
+
def valid?(pan)
|
60
|
+
@checksum_func.call(pan)
|
61
|
+
end
|
62
|
+
|
63
|
+
def numerals_mask?
|
64
|
+
if @mask.to_s =~ /^\d+$/
|
65
|
+
true
|
66
|
+
else
|
67
|
+
false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,308 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
require 'fluent/test/driver/filter'
|
3
|
+
|
4
|
+
require 'fluent/plugin/filter_pan_anonymizer'
|
5
|
+
|
6
|
+
# NOTE: The card number in the test doesn't exist in the world!
|
7
|
+
|
8
|
+
class PANAnonymizerFilterTest < Test::Unit::TestCase
|
9
|
+
def setup
|
10
|
+
Fluent::Test::setup
|
11
|
+
@time = Fluent::Engine.now
|
12
|
+
end
|
13
|
+
|
14
|
+
CONFIG = %[
|
15
|
+
<pan>
|
16
|
+
formats /4\\d{15}/
|
17
|
+
checksum_algorithm luhn
|
18
|
+
mask xxxx
|
19
|
+
</pan>
|
20
|
+
<pan>
|
21
|
+
formats /4\\d{15}/
|
22
|
+
checksum_algorithm none
|
23
|
+
mask xxxx
|
24
|
+
</pan>
|
25
|
+
<pan>
|
26
|
+
formats /4019-\\d{4}-\\d{4}-\\d{4}/
|
27
|
+
checksum_algorithm luhn
|
28
|
+
mask xxxx
|
29
|
+
</pan>
|
30
|
+
<pan>
|
31
|
+
formats /4019\\d{10}/, /4019-\\d{4}-\\d{4}-\\d{4}/
|
32
|
+
checksum_algorithm luhn
|
33
|
+
mask xxxx
|
34
|
+
</pan>
|
35
|
+
ignore_keys ignore1, ignore2
|
36
|
+
]
|
37
|
+
|
38
|
+
def create_driver(conf=CONFIG)
|
39
|
+
Fluent::Test::Driver::Filter.new(Fluent::Plugin::PANAnonymizerFilter).configure(conf)
|
40
|
+
end
|
41
|
+
|
42
|
+
def filter(conf, messages)
|
43
|
+
d = create_driver(conf)
|
44
|
+
d.run(default_tag: 'test') do
|
45
|
+
messages.each do |message|
|
46
|
+
d.feed(message)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
d.filtered_records
|
50
|
+
end
|
51
|
+
|
52
|
+
sub_test_case 'configured with invalid configuration' do
|
53
|
+
test 'empty configuration' do
|
54
|
+
assert_raise(Fluent::ConfigError) do
|
55
|
+
create_driver("")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
test '<pan></pan> is required' do
|
59
|
+
conf = %[
|
60
|
+
]
|
61
|
+
assert_raise(Fluent::ConfigError) do
|
62
|
+
create_driver(conf)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
test 'ok if <pan> exists' do
|
66
|
+
conf = %[
|
67
|
+
<pan>
|
68
|
+
</pan>
|
69
|
+
]
|
70
|
+
assert_nothing_raised(Fluent::ConfigError) do
|
71
|
+
create_driver(conf)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
test 'valid config' do
|
75
|
+
conf = %[
|
76
|
+
<pan>
|
77
|
+
formats /4\d{15}/
|
78
|
+
checksum_algorithm luhn
|
79
|
+
mask xxxx
|
80
|
+
</pan>
|
81
|
+
ignore_keys key1, key2
|
82
|
+
]
|
83
|
+
assert_nothing_raised(Fluent::ConfigError) do
|
84
|
+
create_driver(conf)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
test 'multi <pan> block' do
|
88
|
+
conf = %[
|
89
|
+
<pan>
|
90
|
+
formats /40192491\d{8}/
|
91
|
+
checksum_algorithm luhn
|
92
|
+
mask xxxx
|
93
|
+
</pan>
|
94
|
+
<pan>
|
95
|
+
formats /40192492\d{8}/
|
96
|
+
checksum_algorithm luhn
|
97
|
+
mask xxxx
|
98
|
+
</pan>
|
99
|
+
ignore_keys key1, key2
|
100
|
+
]
|
101
|
+
assert_nothing_raised(Fluent::ConfigError) do
|
102
|
+
create_driver(conf)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
test 'multi <pan> block with multi formats fields' do
|
106
|
+
conf = %[
|
107
|
+
<pan>
|
108
|
+
formats /40192491\d{8}/, /4019-2491-\d{4}-\d{4}/
|
109
|
+
checksum_algorithm luhn
|
110
|
+
mask xxxx
|
111
|
+
</pan>
|
112
|
+
<pan>
|
113
|
+
formats /40192492\d{8}/, /4019-2492-\d{4}-\d{4}/
|
114
|
+
checksum_algorithm luhn
|
115
|
+
mask xxxx
|
116
|
+
</pan>
|
117
|
+
ignore_keys key1, key2
|
118
|
+
]
|
119
|
+
assert_nothing_raised(Fluent::ConfigError) do
|
120
|
+
create_driver(conf)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
sub_test_case 'normal case' do
|
126
|
+
test "in case of nnnnnnnnnnnnnnnn" do
|
127
|
+
conf = %[
|
128
|
+
<pan>
|
129
|
+
formats /4\\d{15}/
|
130
|
+
checksum_algorithm luhn
|
131
|
+
mask xxxx
|
132
|
+
</pan>
|
133
|
+
]
|
134
|
+
messages = [
|
135
|
+
{
|
136
|
+
"key": "9994019249331712145999"
|
137
|
+
}
|
138
|
+
]
|
139
|
+
expected = [
|
140
|
+
{
|
141
|
+
"key": "999xxxx999"
|
142
|
+
}
|
143
|
+
]
|
144
|
+
filtered = filter(conf, messages)
|
145
|
+
assert_equal(expected, filtered)
|
146
|
+
end
|
147
|
+
test "in case of nnnn-nnnn-nnnn-nnnn" do
|
148
|
+
conf = %[
|
149
|
+
<pan>
|
150
|
+
formats /4\\d{3}-\\d{4}-\\d{4}-\\d{4}/
|
151
|
+
checksum_algorithm luhn
|
152
|
+
mask xxxx
|
153
|
+
</pan>
|
154
|
+
]
|
155
|
+
messages = [
|
156
|
+
{
|
157
|
+
"key": "9994019-2493-3171-2145999"
|
158
|
+
}
|
159
|
+
]
|
160
|
+
expected = [
|
161
|
+
{
|
162
|
+
"key": "999xxxx999"
|
163
|
+
}
|
164
|
+
]
|
165
|
+
filtered = filter(conf, messages)
|
166
|
+
assert_equal(expected, filtered)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
sub_test_case 'checksum_algorithm' do
|
171
|
+
test "not be masked if PAN is not satisfied luhn" do
|
172
|
+
conf = %[
|
173
|
+
<pan>
|
174
|
+
formats /4\\d{15}/
|
175
|
+
checksum_algorithm luhn
|
176
|
+
mask xxxx
|
177
|
+
</pan>
|
178
|
+
]
|
179
|
+
messages = [
|
180
|
+
{
|
181
|
+
"key": "9994019111122223333999"
|
182
|
+
}
|
183
|
+
]
|
184
|
+
expected = [
|
185
|
+
{
|
186
|
+
"key": "9994019111122223333999"
|
187
|
+
}
|
188
|
+
]
|
189
|
+
filtered = filter(conf, messages)
|
190
|
+
assert_equal(expected, filtered)
|
191
|
+
end
|
192
|
+
test "be masked if checksum_algorithm is none" do
|
193
|
+
conf = %[
|
194
|
+
<pan>
|
195
|
+
formats /4\\d{15}/
|
196
|
+
checksum_algorithm none
|
197
|
+
mask xxxx
|
198
|
+
</pan>
|
199
|
+
]
|
200
|
+
messages = [
|
201
|
+
{
|
202
|
+
"key": "9994019111122223333999"
|
203
|
+
}
|
204
|
+
]
|
205
|
+
expected = [
|
206
|
+
{
|
207
|
+
"key": "999xxxx999"
|
208
|
+
}
|
209
|
+
]
|
210
|
+
filtered = filter(conf, messages)
|
211
|
+
assert_equal(expected, filtered)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
sub_test_case 'integer value' do
|
216
|
+
test "not be masked if mask is string" do
|
217
|
+
conf = %[
|
218
|
+
<pan>
|
219
|
+
formats /4\\d{15}/
|
220
|
+
checksum_algorithm luhn
|
221
|
+
mask xxxx
|
222
|
+
</pan>
|
223
|
+
]
|
224
|
+
messages = [
|
225
|
+
{
|
226
|
+
"key": 9994019249331712145999
|
227
|
+
}
|
228
|
+
]
|
229
|
+
expected = [
|
230
|
+
{
|
231
|
+
"key": 9994019249331712145999
|
232
|
+
}
|
233
|
+
]
|
234
|
+
filtered = filter(conf, messages)
|
235
|
+
assert_equal(expected, filtered)
|
236
|
+
end
|
237
|
+
test "be masked if force flag exists" do
|
238
|
+
conf = %[
|
239
|
+
<pan>
|
240
|
+
formats /4\\d{15}/
|
241
|
+
checksum_algorithm luhn
|
242
|
+
mask xxxx
|
243
|
+
force true
|
244
|
+
</pan>
|
245
|
+
]
|
246
|
+
messages = [
|
247
|
+
{
|
248
|
+
"key": 9994019249331712145999
|
249
|
+
}
|
250
|
+
]
|
251
|
+
expected = [
|
252
|
+
{
|
253
|
+
"key": "999xxxx999"
|
254
|
+
}
|
255
|
+
]
|
256
|
+
filtered = filter(conf, messages)
|
257
|
+
assert_equal(expected, filtered)
|
258
|
+
end
|
259
|
+
test "be masked if mask is integer value" do
|
260
|
+
conf = %[
|
261
|
+
<pan>
|
262
|
+
formats /4\\d{15}/
|
263
|
+
checksum_algorithm luhn
|
264
|
+
mask 1111111111111111
|
265
|
+
</pan>
|
266
|
+
]
|
267
|
+
messages = [
|
268
|
+
{
|
269
|
+
"key": 9994019249331712145999
|
270
|
+
}
|
271
|
+
]
|
272
|
+
expected = [
|
273
|
+
{
|
274
|
+
"key": 9991111111111111111999
|
275
|
+
}
|
276
|
+
]
|
277
|
+
filtered = filter(conf, messages)
|
278
|
+
assert_equal(expected, filtered)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
sub_test_case 'ignore keys' do
|
283
|
+
test "not be masked" do
|
284
|
+
conf = %[
|
285
|
+
<pan>
|
286
|
+
formats /4\\d{15}/
|
287
|
+
checksum_algorithm luhn
|
288
|
+
mask 9999999999999999
|
289
|
+
</pan>
|
290
|
+
ignore_keys time
|
291
|
+
]
|
292
|
+
messages = [
|
293
|
+
{
|
294
|
+
"time": 40192493317121459,
|
295
|
+
"key": 40192493317121459
|
296
|
+
}
|
297
|
+
]
|
298
|
+
expected = [
|
299
|
+
{
|
300
|
+
"time": 40192493317121459,
|
301
|
+
"key": 99999999999999999
|
302
|
+
}
|
303
|
+
]
|
304
|
+
filtered = filter(conf, messages)
|
305
|
+
assert_equal(expected, filtered)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'fluent/plugin/pan/masker'
|
3
|
+
|
4
|
+
# NOTE: The card number in the test doesn't exist in the world!
|
5
|
+
|
6
|
+
class PANMaskerTest < Test::Unit::TestCase
|
7
|
+
|
8
|
+
sub_test_case "valid?" do
|
9
|
+
test "valid digits" do
|
10
|
+
valid_card_number = [4, 0, 1, 9, 2, 4, 9, 3, 3, 1, 7, 1, 2, 1, 4, 5]
|
11
|
+
f = Fluent::PAN::Masker.new(//, :luhn, "")
|
12
|
+
assert_equal(true, f.valid?(valid_card_number))
|
13
|
+
end
|
14
|
+
|
15
|
+
test "invalid digits" do
|
16
|
+
invalid_card_number = [4, 0, 1, 9, 2, 4, 9, 3, 9, 9, 9, 9, 9, 9, 9, 9]
|
17
|
+
f = Fluent::PAN::Masker.new(//, :luhn, "")
|
18
|
+
assert_equal(false, f.valid?(invalid_card_number))
|
19
|
+
end
|
20
|
+
|
21
|
+
test "always true when checksum algorithm is none" do
|
22
|
+
valid_card_number = [4, 0, 1, 9, 2, 4, 9, 3, 3, 1, 7, 1, 2, 1, 4, 5]
|
23
|
+
f = Fluent::PAN::Masker.new(//, :none, "")
|
24
|
+
assert_equal(true, f.valid?(valid_card_number))
|
25
|
+
|
26
|
+
invalid_card_number = [4, 0, 1, 9, 2, 4, 9, 3, 9, 9, 9, 9, 9, 9, 9, 9]
|
27
|
+
f = Fluent::PAN::Masker.new(//, :none, "")
|
28
|
+
assert_equal(true, f.valid?(invalid_card_number))
|
29
|
+
|
30
|
+
invalid_card_number = [9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
|
31
|
+
f = Fluent::PAN::Masker.new(//, :none, "")
|
32
|
+
assert_equal(true, f.valid?(invalid_card_number))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
sub_test_case "numerals_mask?" do
|
37
|
+
test "true" do
|
38
|
+
mask = 0
|
39
|
+
f = Fluent::PAN::Masker.new(//, :none, mask)
|
40
|
+
assert_equal(true, f.numerals_mask?)
|
41
|
+
|
42
|
+
mask = 00
|
43
|
+
f = Fluent::PAN::Masker.new(//, :none, mask)
|
44
|
+
assert_equal(true, f.numerals_mask?)
|
45
|
+
|
46
|
+
mask = 100
|
47
|
+
f = Fluent::PAN::Masker.new(//, :none, mask)
|
48
|
+
assert_equal(true, f.numerals_mask?)
|
49
|
+
|
50
|
+
mask = "0"
|
51
|
+
f = Fluent::PAN::Masker.new(//, :none, mask)
|
52
|
+
assert_equal(true, f.numerals_mask?)
|
53
|
+
|
54
|
+
mask = "00"
|
55
|
+
f = Fluent::PAN::Masker.new(//, :none, mask)
|
56
|
+
assert_equal(true, f.numerals_mask?)
|
57
|
+
|
58
|
+
mask = "100"
|
59
|
+
f = Fluent::PAN::Masker.new(//, :none, mask)
|
60
|
+
assert_equal(true, f.numerals_mask?)
|
61
|
+
end
|
62
|
+
|
63
|
+
test "false" do
|
64
|
+
mask = "*"
|
65
|
+
f = Fluent::PAN::Masker.new(//, :none, mask)
|
66
|
+
assert_equal(false, f.numerals_mask?)
|
67
|
+
|
68
|
+
mask = "*00"
|
69
|
+
f = Fluent::PAN::Masker.new(//, :none, mask)
|
70
|
+
assert_equal(false, f.numerals_mask?)
|
71
|
+
|
72
|
+
mask = "100*"
|
73
|
+
f = Fluent::PAN::Masker.new(//, :none, mask)
|
74
|
+
assert_equal(false, f.numerals_mask?)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
sub_test_case "mask_if_pan_found?" do
|
79
|
+
test "with numerals string mask" do
|
80
|
+
mask = "0000000000000000"
|
81
|
+
f = Fluent::PAN::Masker.new(/4\d{15}/, :luhn, mask)
|
82
|
+
|
83
|
+
filtered = f.mask_if_found_pan("4019249331712145")
|
84
|
+
assert_equal(String, filtered.class)
|
85
|
+
assert_equal("#{mask}", filtered)
|
86
|
+
|
87
|
+
filtered = f.mask_if_found_pan("XXXX4019249331712145XXXX")
|
88
|
+
assert_equal(String, filtered.class)
|
89
|
+
assert_equal("XXXX#{mask}XXXX", filtered)
|
90
|
+
|
91
|
+
filtered = f.mask_if_found_pan(4019249331712145)
|
92
|
+
assert_equal(Integer, filtered.class)
|
93
|
+
assert_equal("#{mask}".to_i, filtered)
|
94
|
+
|
95
|
+
filtered = f.mask_if_found_pan(140192493317121459)
|
96
|
+
assert_equal(Integer, filtered.class)
|
97
|
+
assert_equal("1#{mask}9".to_i, filtered)
|
98
|
+
end
|
99
|
+
|
100
|
+
test "with numerals mask" do
|
101
|
+
mask = 4019111111111111
|
102
|
+
f = Fluent::PAN::Masker.new(/4\d{15}/, :luhn, mask)
|
103
|
+
|
104
|
+
filtered = f.mask_if_found_pan("4019249331712145")
|
105
|
+
assert_equal(String, filtered.class)
|
106
|
+
assert_equal("#{mask}", filtered)
|
107
|
+
|
108
|
+
filtered = f.mask_if_found_pan("XXXX4019249331712145XXXX")
|
109
|
+
assert_equal(String, filtered.class)
|
110
|
+
assert_equal("XXXX#{mask}XXXX", filtered)
|
111
|
+
|
112
|
+
filtered = f.mask_if_found_pan(4019249331712145)
|
113
|
+
assert_equal(Integer, filtered.class)
|
114
|
+
assert_equal("#{mask}".to_i, filtered)
|
115
|
+
|
116
|
+
filtered = f.mask_if_found_pan(140192493317121459)
|
117
|
+
assert_equal(Integer, filtered.class)
|
118
|
+
assert_equal("1#{mask}9".to_i, filtered)
|
119
|
+
end
|
120
|
+
|
121
|
+
test "with 0000000000000000 mask" do
|
122
|
+
mask = 0000000000000000
|
123
|
+
f = Fluent::PAN::Masker.new(/4\d{15}/, :luhn, mask)
|
124
|
+
|
125
|
+
filtered = f.mask_if_found_pan("4019249331712145")
|
126
|
+
assert_equal(String, filtered.class)
|
127
|
+
assert_equal("0", filtered)
|
128
|
+
|
129
|
+
filtered = f.mask_if_found_pan("XXXX4019249331712145XXXX")
|
130
|
+
assert_equal(String, filtered.class)
|
131
|
+
assert_equal("XXXX0XXXX", filtered)
|
132
|
+
|
133
|
+
filtered = f.mask_if_found_pan(4019249331712145)
|
134
|
+
assert_equal(Integer, filtered.class)
|
135
|
+
assert_equal(0, filtered)
|
136
|
+
|
137
|
+
filtered = f.mask_if_found_pan(140192493317121459)
|
138
|
+
assert_equal(Integer, filtered.class)
|
139
|
+
assert_equal(109, filtered)
|
140
|
+
end
|
141
|
+
|
142
|
+
test "with string mask" do
|
143
|
+
mask = "****"
|
144
|
+
f = Fluent::PAN::Masker.new(/4\d{15}/, :luhn, mask)
|
145
|
+
|
146
|
+
filtered = f.mask_if_found_pan("4019249331712145")
|
147
|
+
assert_equal(String, filtered.class)
|
148
|
+
assert_equal("#{mask}", filtered)
|
149
|
+
|
150
|
+
filtered = f.mask_if_found_pan("XXXX4019249331712145XXXX")
|
151
|
+
assert_equal(String, filtered.class)
|
152
|
+
assert_equal("XXXX#{mask}XXXX", filtered)
|
153
|
+
|
154
|
+
filtered = f.mask_if_found_pan(4019249331712145)
|
155
|
+
assert_equal(Integer, filtered.class)
|
156
|
+
assert_equal(4019249331712145, filtered)
|
157
|
+
|
158
|
+
filtered = f.mask_if_found_pan(140192493317121459)
|
159
|
+
assert_equal(Integer, filtered.class)
|
160
|
+
assert_equal(140192493317121459, filtered)
|
161
|
+
end
|
162
|
+
|
163
|
+
test "with string mask and force: true" do
|
164
|
+
mask = "****"
|
165
|
+
f = Fluent::PAN::Masker.new(/4\d{15}/, :luhn, mask, force: true)
|
166
|
+
|
167
|
+
filtered = f.mask_if_found_pan("4019249331712145")
|
168
|
+
assert_equal(String, filtered.class)
|
169
|
+
assert_equal("#{mask}", filtered)
|
170
|
+
|
171
|
+
filtered = f.mask_if_found_pan("XXXX4019249331712145XXXX")
|
172
|
+
assert_equal(String, filtered.class)
|
173
|
+
assert_equal("XXXX#{mask}XXXX", filtered)
|
174
|
+
|
175
|
+
filtered = f.mask_if_found_pan(4019249331712145)
|
176
|
+
assert_equal(String, filtered.class)
|
177
|
+
assert_equal("#{mask}", filtered)
|
178
|
+
|
179
|
+
filtered = f.mask_if_found_pan(140192493317121459)
|
180
|
+
assert_equal(String, filtered.class)
|
181
|
+
assert_equal("1#{mask}9", filtered)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fluent-plugin-pan-anonymizer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hiroaki Sano
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-11-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: test-unit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: fluentd
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.14.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.14.0
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- hiroaki.sano.9stories@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- Gemfile
|
78
|
+
- LICENSE
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- fluent-plugin-pan-anonymizer.gemspec
|
82
|
+
- lib/fluent/plugin/filter_pan_anonymizer.rb
|
83
|
+
- lib/fluent/plugin/pan/masker.rb
|
84
|
+
- test/helper.rb
|
85
|
+
- test/plugin/test_filter_pan_anonymizer.rb
|
86
|
+
- test/plugin/test_pan_masker.rb
|
87
|
+
homepage: https://github.com/kanmu/fluent-plugin-pan-anonymizer
|
88
|
+
licenses:
|
89
|
+
- Apache License, Version 2.0
|
90
|
+
metadata: {}
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
requirements: []
|
106
|
+
rubyforge_project:
|
107
|
+
rubygems_version: 2.6.14.1
|
108
|
+
signing_key:
|
109
|
+
specification_version: 4
|
110
|
+
summary: Fluentd filter plugin to anonymize credit card numbers.
|
111
|
+
test_files:
|
112
|
+
- test/helper.rb
|
113
|
+
- test/plugin/test_filter_pan_anonymizer.rb
|
114
|
+
- test/plugin/test_pan_masker.rb
|