sanctum 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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