masker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,8 @@
1
+ require 'yaml'
2
+
3
+ module Configuration
4
+ def self.load(config_path)
5
+ fail "File not found: #{config_path}" unless File.exist?(config_path)
6
+ YAML.load_file(config_path)
7
+ end
8
+ 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
@@ -0,0 +1,5 @@
1
+ class NullObject
2
+ def method_missing(*args, &block)
3
+ self
4
+ end
5
+ 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: []