masker 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 +7 -0
- data/lib/masker.rb +20 -0
- data/lib/masker/adapters/postgres.rb +83 -0
- data/lib/masker/configuration.rb +8 -0
- data/lib/masker/configurations/postgres.rb +77 -0
- data/lib/masker/data_generator.rb +50 -0
- data/lib/masker/null_object.rb +5 -0
- metadata +82 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2d1fd50722f8660778d1e9675aa49e77d421ca86
|
4
|
+
data.tar.gz: 2a55877fa12a4458cd500a8319e2d24d868406a0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0443e9b1df55fa2c2c424be37359936ac341bfacd9bff884daedaf22fde819464c340b3a8dbc9b3adaa62a3337b8a74b5808fa48ed22a111bb5bb0c70c489b55
|
7
|
+
data.tar.gz: 65ebaf8019304fc640c64cc8cb8a07546143fa981a7752103913cce1dd451bfea9fe20e45ed0aa7b16a829c9644d128591b66567454f1a485e1ef57d7469e7ac
|
data/lib/masker.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'masker/adapters/postgres'
|
2
|
+
require 'masker/configurations/postgres'
|
3
|
+
require 'masker/null_object'
|
4
|
+
require 'masker/configuration'
|
5
|
+
require 'masker/data_generator'
|
6
|
+
require 'pg'
|
7
|
+
|
8
|
+
class Masker
|
9
|
+
def initialize(database_url:, config_path:, logger: NullObject.new, opts: {})
|
10
|
+
@adapter = Adapters::Postgres.new(database_url, config_path, logger, opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
def mask
|
14
|
+
adapter.mask
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :adapter
|
20
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
class Masker
|
2
|
+
module Adapters
|
3
|
+
class Postgres
|
4
|
+
def initialize(database_url, config_path, logger, opts = {})
|
5
|
+
@conn = PG.connect(database_url)
|
6
|
+
@config = Configurations::Postgres.new(conn, config_path, logger, opts)
|
7
|
+
@logger = logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def mask
|
11
|
+
remove_temp_tables
|
12
|
+
config.remove_missing_tables
|
13
|
+
config.remove_missing_columns
|
14
|
+
create_temp_tables
|
15
|
+
insert_fake_data_into_temp_tables
|
16
|
+
merge_tables
|
17
|
+
truncate
|
18
|
+
ensure
|
19
|
+
remove_temp_tables
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def remove_temp_tables
|
25
|
+
tables.keys.each do |table_name|
|
26
|
+
conn.exec("DROP TABLE IF EXISTS temp_#{table_name};")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_temp_tables
|
31
|
+
tables.keys.each do |table_name|
|
32
|
+
conn.exec("CREATE TABLE temp_#{table_name} AS SELECT * FROM #{table_name} LIMIT 0;")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def insert_fake_data_into_temp_tables
|
37
|
+
tables.each do |table, columns|
|
38
|
+
logger.info "Masking #{table}..."
|
39
|
+
conn.transaction do |conn|
|
40
|
+
config.ids_to_mask[table].each_slice(1000) do |ids|
|
41
|
+
fake_rows = create_fake_rows(ids, columns)
|
42
|
+
conn.exec("INSERT INTO temp_#{table} (id, #{columns.keys.join(", ")}) VALUES #{fake_rows};")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_fake_rows(ids, columns)
|
49
|
+
ids.map { |id| "(#{create_fake_row(id, columns)})" }.join(", ")
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_fake_row(id, columns)
|
53
|
+
columns.map { |_, mask_type| stringify(DataGenerator.generate(mask_type)) }
|
54
|
+
.unshift(id)
|
55
|
+
.join(", ")
|
56
|
+
end
|
57
|
+
|
58
|
+
def stringify(value)
|
59
|
+
value.nil? ? 'NULL' : "'#{conn.escape_string(value.to_s)}'"
|
60
|
+
end
|
61
|
+
|
62
|
+
def merge_tables
|
63
|
+
tables.each do |table, columns|
|
64
|
+
set_statement = columns.keys.map { |column| "#{column} = temp_#{table}.#{column}" }.join(", ")
|
65
|
+
conn.exec("UPDATE #{table} SET #{set_statement} FROM temp_#{table} WHERE #{table}.id = temp_#{table}.id;")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def truncate
|
70
|
+
config.tables_to_truncate.each do |table|
|
71
|
+
logger.info "Truncating #{table}..."
|
72
|
+
conn.exec("TRUNCATE #{table};")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def tables
|
77
|
+
config.tables
|
78
|
+
end
|
79
|
+
|
80
|
+
attr_reader :logger, :config, :conn
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
class Masker
|
2
|
+
module Configurations
|
3
|
+
class Postgres
|
4
|
+
attr_reader :tables
|
5
|
+
|
6
|
+
def initialize(conn, config_path, logger, opts = {})
|
7
|
+
@config = Configuration.load(config_path)
|
8
|
+
@conn = conn
|
9
|
+
@logger = logger
|
10
|
+
@opts = opts
|
11
|
+
@tables = config['mask']
|
12
|
+
end
|
13
|
+
|
14
|
+
def ids_to_mask
|
15
|
+
@ids_to_mask ||=
|
16
|
+
tables.keys.each_with_object(Hash.new { |k, v| k[v] = [] }) do |table, ids|
|
17
|
+
conn.exec("SELECT id FROM #{table};") do |result|
|
18
|
+
ids[table].concat(result.values.flatten - Array(opts.dig(:safe_ids, table.to_sym)).map(&:to_s))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def missing_tables
|
24
|
+
tables.keys.each_with_object([]) do |table_name, missing_tables|
|
25
|
+
conn.exec("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = '#{table_name}');") do |result|
|
26
|
+
if result[0]['exists'] == 'f'
|
27
|
+
missing_tables << table_name
|
28
|
+
logger.warn "Table: #{table_name} exists in configuration but not in database."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def missing_columns
|
35
|
+
tables.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(table_name, columns), missing_columns|
|
36
|
+
columns.keys.each do |column_name|
|
37
|
+
sql = <<~SQL
|
38
|
+
SELECT EXISTS (
|
39
|
+
SELECT 1 FROM information_schema.columns
|
40
|
+
WHERE table_name='#{table_name}'
|
41
|
+
AND column_name='#{column_name}'
|
42
|
+
);
|
43
|
+
SQL
|
44
|
+
conn.exec(sql) do |result|
|
45
|
+
if result[0]['exists'] == 'f'
|
46
|
+
missing_columns[table_name] << column_name
|
47
|
+
logger.warn "Column: #{table_name}:#{column_name} exists in configuration but not in database."
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def remove_missing_tables
|
55
|
+
missing_tables.each do |table|
|
56
|
+
tables.delete(table)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def remove_missing_columns
|
61
|
+
missing_columns.each do |table, columns|
|
62
|
+
columns.each do |column|
|
63
|
+
tables[table].delete(column)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def tables_to_truncate
|
69
|
+
config['truncate']
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
attr_reader :config, :opts, :conn, :logger
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'faker'
|
2
|
+
|
3
|
+
module DataGenerator
|
4
|
+
class << self
|
5
|
+
def generate(type)
|
6
|
+
case type
|
7
|
+
when :name
|
8
|
+
Faker::Name.name
|
9
|
+
when :company_name
|
10
|
+
Faker::Company.name
|
11
|
+
when :first_name
|
12
|
+
Faker::Name.first_name
|
13
|
+
when :last_name
|
14
|
+
Faker::Name.last_name
|
15
|
+
when :email
|
16
|
+
"#{SecureRandom.hex(8).upcase}_#{Faker::Internet.email}"
|
17
|
+
when :text
|
18
|
+
Faker::Lorem.sentence
|
19
|
+
when :date
|
20
|
+
Faker::Date.forward(1000)
|
21
|
+
when :city
|
22
|
+
"#{Faker::Address.city}_#{SecureRandom.hex(8).upcase}"
|
23
|
+
when :domain_name
|
24
|
+
Faker::Internet.domain_name
|
25
|
+
when :country
|
26
|
+
"#{Faker::Address.country}_#{SecureRandom.hex(8).upcase}"
|
27
|
+
when :characters
|
28
|
+
Faker::Lorem.characters(10)
|
29
|
+
when :zip_code
|
30
|
+
Faker::Address.zip_code
|
31
|
+
when :year
|
32
|
+
Faker::Number.between(1900, 2020)
|
33
|
+
when :integer
|
34
|
+
Faker::Number.number(8)
|
35
|
+
when :low_integer
|
36
|
+
Faker::Number.between(1, 200)
|
37
|
+
when :float
|
38
|
+
Faker::Number.decimal(2, 2)
|
39
|
+
when :state
|
40
|
+
"#{Faker::Address.state}_#{SecureRandom.hex(8).upcase}"
|
41
|
+
when :phone
|
42
|
+
Faker::Number.number(10)
|
43
|
+
when :street_address
|
44
|
+
Faker::Address.street_address
|
45
|
+
else
|
46
|
+
type
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: masker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Danny Park
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pg
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.21'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.21'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faker
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.8'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.8'
|
41
|
+
description: |-
|
42
|
+
Production databases contain sensitive information that should not be
|
43
|
+
propagated down to other environments. This gem allows users to create
|
44
|
+
masking strategies in a YML file that specify columns to mask and tables
|
45
|
+
to truncate
|
46
|
+
email:
|
47
|
+
- dannypark92@gmail.com
|
48
|
+
executables: []
|
49
|
+
extensions: []
|
50
|
+
extra_rdoc_files: []
|
51
|
+
files:
|
52
|
+
- lib/masker.rb
|
53
|
+
- lib/masker/adapters/postgres.rb
|
54
|
+
- lib/masker/configuration.rb
|
55
|
+
- lib/masker/configurations/postgres.rb
|
56
|
+
- lib/masker/data_generator.rb
|
57
|
+
- lib/masker/null_object.rb
|
58
|
+
homepage: https://www.github.com/viewthespace/masker
|
59
|
+
licenses:
|
60
|
+
- MIT
|
61
|
+
metadata: {}
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 2.6.13
|
79
|
+
signing_key:
|
80
|
+
specification_version: 4
|
81
|
+
summary: Database masking for sensitive information
|
82
|
+
test_files: []
|