sanctum 0.8.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.
@@ -0,0 +1,29 @@
1
+ module Sanctum
2
+ module Command
3
+ class Check < Base
4
+
5
+ def run
6
+ targets.each do |target|
7
+ # Recursively get local files for each prefix specified in sanctum.yaml
8
+ local_paths = get_local_paths(File.join(File.dirname(config_file), target[:path]))
9
+ # Read each file
10
+ local_secrets = read_local_files(local_paths)
11
+ # Decrypt each secret
12
+ local_secrets = VaultTransit.decrypt(vault_client, local_secrets, transit_key)
13
+
14
+ # Recursively get vault secrets for each prefix specified in sanctum.yaml
15
+ secrets_list = VaultSecrets.new(vault_client, target[:prefix]).get
16
+
17
+ # Only one entry in this hash (which will be the target).
18
+ tree = secrets_list.values.first
19
+ # Build local paths based on prefix and paths specified in sanctum.yaml
20
+ vault_secrets = build_path(tree, [target[:path]])
21
+ # Join the path array to create a path
22
+ vault_secrets = join_path(vault_secrets, config_file)
23
+ compare_secrets(vault_secrets, local_secrets, target[:name])
24
+ end
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ require 'fileutils'
2
+
3
+ module Sanctum
4
+ module Command
5
+ # Intentionally not extending Base
6
+ # This command creates an example config
7
+ class Config
8
+ include Colorizer
9
+ attr_reader :config_path, :example_file
10
+
11
+ def initialize(options={}, _args=[])
12
+ options = {working_dir: Dir.pwd}.merge(options)
13
+
14
+ relative_path = File.expand_path File.dirname(__FILE__)
15
+ @config_path = "#{options[:working_dir]}/sanctum.yaml"
16
+ @example_file = "#{relative_path}/sanctum.example.yaml"
17
+ end
18
+
19
+ def run
20
+ raise yellow("config file already exists") if config_exist?
21
+ create_config_file
22
+ end
23
+
24
+ def config_exist?
25
+ File.file?(config_path)
26
+ end
27
+
28
+ def create_config_file
29
+ FileUtils.cp(example_file, config_path)
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,62 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'pathname'
4
+ require 'tempfile'
5
+ require 'yaml'
6
+
7
+ module Sanctum
8
+ module Command
9
+ class Create < Base
10
+
11
+ def run(&block)
12
+ if args.one?
13
+ path = args[0]
14
+ validate_path(path)
15
+ create_file(path, &block)
16
+ else
17
+ raise ArgumentError, red('Please pass only one path argument')
18
+ end
19
+ end
20
+
21
+ private
22
+ def create_file(path)
23
+ # Calling vault_client will help prevent a race condition where the token is expired
24
+ # and contents fail to encrypt
25
+ vault_client
26
+ tmp_file = Tempfile.new(File.basename(path))
27
+
28
+ begin
29
+ if block_given?
30
+ yield tmp_file
31
+ else
32
+ editor = ENV.fetch('EDITOR', 'vi')
33
+ raise red("Error with editor") unless system(editor, tmp_file.path)
34
+ end
35
+
36
+ contents = File.read(tmp_file.path)
37
+ data_hash = {"#{tmp_file.path}" => validate(contents)}
38
+ write_encrypted_data(vault_client, data_hash, transit_key)
39
+ tmp_file.close
40
+
41
+ FileUtils.cp(tmp_file.path, path)
42
+ rescue Exception => e
43
+ # If write_encrypted_data failed, data would fail to write to disk
44
+ # It would be sad to lose that data, at least this would print the contents to the console.
45
+ puts red("Contents may have failed to write\nError: #{e}")
46
+ puts yellow("Contents: \n#{contents}")
47
+ ensure
48
+ tmp_file.close
49
+ secure_erase(tmp_file.path, tmp_file.length)
50
+ tmp_file.unlink
51
+ end
52
+ end
53
+
54
+ def validate_path(path)
55
+ path = Pathname.new(path)
56
+ raise yellow("File exists, use edit command") if path.exist?
57
+ path.dirname.mkpath unless path.dirname.exist?
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,70 @@
1
+ require 'hashdiff'
2
+
3
+ module Sanctum
4
+ module Command
5
+ module DiffHelper
6
+
7
+ def hash_diff(first_hash, second_hash)
8
+ differences = HashDiff.best_diff(first_hash, second_hash, delimiter: " => ", array_path: true)
9
+
10
+ differences.each do |diff|
11
+ if diff[0] == "+"
12
+ puts green("#{diff[0].to_s + diff[1].join(" => ").to_s} => #{diff[2]}")
13
+ else
14
+ puts red("#{diff[0].to_s + diff[1].join(" => ").to_s} => #{diff[2]}")
15
+ end
16
+ end
17
+ differences
18
+ end
19
+
20
+ def compare_secrets(vault_secrets, local_secrets, name, direction="both")
21
+ if vault_secrets == local_secrets
22
+ warn yellow("Target #{name}: contains no differences")
23
+ else
24
+ case direction
25
+ when "pull"
26
+ puts yellow("Target #{name}: differences pulling from vault")
27
+ hash_diff(local_secrets, vault_secrets)
28
+ when "push"
29
+ puts yellow("Target #{name}: differences pushing to vault")
30
+ hash_diff(vault_secrets, local_secrets)
31
+ when "both"
32
+ puts yellow("Target #{name}: differences pulling from vault")
33
+ hash_diff(local_secrets, vault_secrets)
34
+
35
+ puts yellow("Target #{name}: differences pushing to vault")
36
+ hash_diff(vault_secrets, local_secrets)
37
+ end
38
+ end
39
+ end
40
+
41
+ def confirmed_with_user?
42
+ puts yellow("\nWould you like to continue?: ")
43
+ question = STDIN.gets.chomp.upcase
44
+
45
+ if ["Y", "YES"].include? question
46
+ puts yellow("Overwriting differences")
47
+ true
48
+ else
49
+ warn yellow("Skipping....\n")
50
+ false
51
+ end
52
+ end
53
+
54
+ # Array is a unique list of paths built from the differences of hash1 and hash2.
55
+ # See diff_paths variable in push or pull command
56
+ # Hash will be all local, or vault secrets.
57
+ # We then build a new hash that contains only the k,v needed to sync (to or from vault)
58
+ def only_changes(array, hash)
59
+ tmp_hash = Hash.new
60
+ array.each do |a|
61
+ hash.each do |k, v|
62
+ tmp_hash[k] = v if a == k
63
+ end
64
+ end
65
+ tmp_hash
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,63 @@
1
+ require 'fileutils'
2
+ require 'tempfile'
3
+ require 'yaml'
4
+ require 'json'
5
+
6
+ module Sanctum
7
+ module Command
8
+ class Edit < Base
9
+
10
+ def run(&block)
11
+ if args.one?
12
+ path = args[0]
13
+ edit_file(path, &block)
14
+ else
15
+ raise ArgumentError, red('Please pass only one path argument')
16
+ end
17
+ end
18
+
19
+ private
20
+ def edit_file(path)
21
+ tmp_file = Tempfile.new(File.basename(path))
22
+
23
+ begin
24
+ encrypted_data_hash = {path => File.read(path)}
25
+ decrypted_data_hash = decrypt_data(vault_client, encrypted_data_hash, transit_key)
26
+ decrypted_data_hash.each_value do |v|
27
+ v = v.to_yaml
28
+ File.write(tmp_file.path, v)
29
+ end
30
+
31
+ if block_given?
32
+ yield tmp_file
33
+ else
34
+ previous_contents = File.read(tmp_file.path)
35
+ editor = ENV.fetch('EDITOR', 'vi')
36
+ raise red("Error with editor") unless system(editor, tmp_file.path )
37
+ end
38
+ contents = File.read(tmp_file.path)
39
+
40
+ # Only write contents if something changed
41
+ unless contents == previous_contents
42
+ data_hash = {"#{tmp_file.path}" => validate(contents)}
43
+ write_encrypted_data(vault_client, data_hash, transit_key)
44
+ tmp_file.close
45
+
46
+ FileUtils.cp(tmp_file.path, path)
47
+ end
48
+
49
+ rescue Exception => e
50
+ # If write_encrypted_data failed, data would fail to write to disk
51
+ # It would be sad to lose that data, at least this would print the contents to the console.
52
+ puts red("Contents may have failed to write\nError: #{e}")
53
+ puts yellow("Contents: \n#{contents}")
54
+ ensure
55
+ tmp_file.close
56
+ secure_erase(tmp_file.path, tmp_file.length)
57
+ tmp_file.unlink
58
+ end
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,58 @@
1
+ require 'securerandom'
2
+ require 'yaml'
3
+ require 'json'
4
+
5
+ module Sanctum
6
+ module Command
7
+ module EditorHelper
8
+ include Colorizer
9
+
10
+ def decrypt_data(vault_client, data, transit_key)
11
+ VaultTransit.decrypt(vault_client, data, transit_key)
12
+ end
13
+
14
+ def write_encrypted_data(vault_client, data, transit_key)
15
+ VaultTransit.write_to_file(vault_client, data, transit_key)
16
+ end
17
+
18
+ def validate(contents)
19
+ validate_json(contents) || validate_yaml(contents) || raise
20
+ rescue
21
+ fail red("Invalid Contents")
22
+ end
23
+
24
+ def validate_json(json)
25
+ JSON.parse(json)
26
+ rescue JSON::ParserError
27
+ nil
28
+ end
29
+
30
+ def validate_yaml(yaml)
31
+ YAML.load(yaml)
32
+ rescue YAML::SyntaxError
33
+ nil
34
+ end
35
+
36
+ def write_random_data(file, file_len)
37
+ max_chunk_len = [file_len, (1024 * 1024 * 2)].max
38
+
39
+ 3.times do
40
+ random_data = SecureRandom.random_bytes(max_chunk_len)
41
+ File.write(file, random_data, 0, mode: 'wb')
42
+ end
43
+ end
44
+
45
+ def secure_erase(file, file_len)
46
+ if file_len >= 1
47
+ begin
48
+ #Try to use shred if available on system
49
+ raise red("Failed system shred") unless system("shred", file)
50
+ rescue
51
+ write_random_data(file, file_len)
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,64 @@
1
+ require 'find'
2
+ require 'pathname'
3
+
4
+ module Sanctum
5
+ module Command
6
+ module PathsHelper
7
+
8
+ #Helper methods for building, reading and joining paths
9
+ private
10
+ def build_path_helper(hash, path = [])
11
+ if hash.values.any? { |k| !k.is_a?(Hash) }
12
+ [path, hash]
13
+ else
14
+ hash.flat_map do |(key,value)|
15
+ build_path_helper(value, path+[key])
16
+ end
17
+ end
18
+ end
19
+
20
+ public
21
+ def build_path(hash, path = [])
22
+ build_path_helper(hash, path).each_slice(2).to_h
23
+ end
24
+
25
+ def join_path(hash, config_file)
26
+ config_file = Pathname.new(config_file)
27
+ tmp_hash = Hash.new
28
+
29
+ hash.each do |p, v|
30
+ p = config_file.dirname + Pathname.new(p.join("/"))
31
+ tmp_hash["#{p}"] = v
32
+ end
33
+ tmp_hash
34
+ end
35
+
36
+ def read_local_files(paths)
37
+ tmp_hash = Hash.new
38
+ paths.each do |k,v|
39
+ if File.file?(k)
40
+ v = File.read(k)
41
+ tmp_hash["#{k}"] = v
42
+ end
43
+ end
44
+ tmp_hash
45
+ end
46
+
47
+ def get_local_paths(paths)
48
+ tmp_array = Array.new
49
+ Find.find(paths) do |path|
50
+ if FileTest.file?(path)
51
+ tmp_array << path
52
+ if File.basename(path).start_with?(?.)
53
+ Find.prune
54
+ else
55
+ next
56
+ end
57
+ end
58
+ end
59
+ tmp_array
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,81 @@
1
+ require 'pathname'
2
+
3
+ module Sanctum
4
+ module Command
5
+ class Pull < Base
6
+
7
+ def run
8
+ targets.each do |target|
9
+ # Use command line if force: true
10
+ if options[:cli][:force]
11
+ force = options[:cli][:force]
12
+ else
13
+ force = target.fetch(:force) {options[:sanctum][:force]}
14
+ end
15
+
16
+ # Recursively get vault secrets for each prefix specified in sanctum.yaml
17
+ secrets_list = VaultSecrets.new(vault_client, target[:prefix]).get
18
+ secrets_list.each do |k,v|
19
+
20
+ vault_secrets = build_vault_secrets(v, [target[:path]])
21
+ local_secrets = build_local_secrets(vault_secrets)
22
+
23
+ #Compare secrets, if there are no differences continue to next target
24
+ differences = compare_secrets(vault_secrets, local_secrets, target[:name], "pull")
25
+ next if differences.nil?
26
+
27
+ #Get uniq array of HashDiff returned paths
28
+ diff_paths = differences.map{|x| x[1][0]}.uniq
29
+
30
+ #Only sync the differences
31
+ vault_secrets = only_changes(diff_paths, vault_secrets)
32
+
33
+ if force
34
+ # Write files to disk and encrypt with transit
35
+ warn red("#{target[:name]}: Forcefully writing differences to disk(pull)")
36
+ VaultTransit.write_to_file(vault_client, vault_secrets, transit_key)
37
+ else
38
+ #Confirm with user, and write to local file if approved
39
+ next unless confirmed_with_user?
40
+ VaultTransit.write_to_file(vault_client, vault_secrets, transit_key)
41
+ end
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ def create_paths(paths)
48
+ paths.each do |k,v|
49
+ k = Pathname.new(k)
50
+ unless k.dirname.exist?
51
+ k.dirname.mkpath
52
+ end
53
+ end
54
+ end
55
+
56
+ def build_vault_secrets(tree, path)
57
+ # Build local paths based on vault_prefix(tree) and paths specified in sanctum.yaml
58
+ vault_secrets = build_path(tree, path)
59
+
60
+ # Join the path array to create a path
61
+ vault_secrets = join_path(vault_secrets, config_file)
62
+
63
+ # Ensure local paths exist, relative to sanctum.yaml if they don't create them
64
+ create_paths(vault_secrets)
65
+ vault_secrets
66
+ end
67
+
68
+ def build_local_secrets(vault_secrets)
69
+
70
+ # read_local_files uses vault_secrets paths to create a new hash with local paths and values.
71
+ # This means that we will only compare secrets/paths that exist in both vault and locally.
72
+ # We will not for example, see differences if a file exists locally but not in vault.
73
+ local_secrets = read_local_files(vault_secrets)
74
+ # Decrypt local_secrets
75
+ local_secrets = VaultTransit.decrypt(vault_client, local_secrets, transit_key)
76
+ local_secrets
77
+ end
78
+
79
+ end
80
+ end
81
+ end