vaultkit 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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +961 -0
  3. data/bin/funl +0 -0
  4. data/bin/vkit +30 -0
  5. data/lib/vkit/cli/api/client.rb +115 -0
  6. data/lib/vkit/cli/base_cli.rb +173 -0
  7. data/lib/vkit/cli/commands/approval_command.rb +94 -0
  8. data/lib/vkit/cli/commands/base_command.rb +42 -0
  9. data/lib/vkit/cli/commands/datasource_command.rb +93 -0
  10. data/lib/vkit/cli/commands/fetch_command.rb +48 -0
  11. data/lib/vkit/cli/commands/login_command.rb +136 -0
  12. data/lib/vkit/cli/commands/logout_command.rb +12 -0
  13. data/lib/vkit/cli/commands/policy_bundle_command.rb +62 -0
  14. data/lib/vkit/cli/commands/policy_deploy_command.rb +32 -0
  15. data/lib/vkit/cli/commands/policy_validate_command.rb +31 -0
  16. data/lib/vkit/cli/commands/request_command.rb +102 -0
  17. data/lib/vkit/cli/commands/requests_list_command.rb +47 -0
  18. data/lib/vkit/cli/commands/scan_command.rb +47 -0
  19. data/lib/vkit/cli/commands/whoami_command.rb +14 -0
  20. data/lib/vkit/cli/commands.rb +5 -0
  21. data/lib/vkit/cli/errors.rb +6 -0
  22. data/lib/vkit/cli/policy_bundle_validator.rb +71 -0
  23. data/lib/vkit/cli/requests_cli.rb +23 -0
  24. data/lib/vkit/cli.rb +4 -0
  25. data/lib/vkit/core/auth_client.rb +104 -0
  26. data/lib/vkit/core/credential_resolver.rb +37 -0
  27. data/lib/vkit/core/credential_store.rb +186 -0
  28. data/lib/vkit/core/table_formatter.rb +36 -0
  29. data/lib/vkit/policy/bundle_compiler.rb +154 -0
  30. data/lib/vkit/policy/schema/policy_bundle.schema.json +296 -0
  31. data/lib/vkit/policy/validate_bundle.rb +37 -0
  32. data/lib/vkit/utils/banner.rb +0 -0
  33. data/lib/vkit/utils/config_loader.rb +0 -0
  34. data/lib/vkit/utils/logger.rb +0 -0
  35. data/lib/vkit.rb +3 -0
  36. metadata +94 -0
@@ -0,0 +1,136 @@
1
+ require "io/console"
2
+ require_relative "../../core/auth_client"
3
+ require_relative "../../core/credential_store"
4
+ require "base64"
5
+ require "json"
6
+
7
+ module Vkit
8
+ module CLI
9
+ module Commands
10
+ class LoginCommand < BaseCommand
11
+ def requires_auth?
12
+ false
13
+ end
14
+
15
+ def initialize(endpoint: nil, email: nil)
16
+ @endpoint = endpoint
17
+ @email = email
18
+ end
19
+
20
+ def call
21
+ endpoint =
22
+ @endpoint ||
23
+ ENV["VKIT_ENDPOINT"] ||
24
+ credential_store.endpoint ||
25
+ prompt("VaultKit Control Plane URL")
26
+ client = Vkit::Core::AuthClient.new(base_url: endpoint)
27
+
28
+ discovery = client.discover
29
+ auth = discovery["preferred"]
30
+
31
+ result =
32
+ case auth
33
+ when "oidc"
34
+ oidc_flow(client, discovery["oidc"]["login_url"])
35
+ when "password"
36
+ password_flow(client)
37
+ when "token"
38
+ token_flow(client)
39
+ else
40
+ raise "Unsupported auth mode: #{auth}"
41
+ end
42
+
43
+ store = Vkit::Core::CredentialStore.new
44
+ store.save(
45
+ endpoint: endpoint,
46
+ token: result[:token],
47
+ user: result[:user]
48
+ )
49
+
50
+ puts "āœ… Logged in as #{result[:user]['email']}"
51
+ rescue => e
52
+ puts "āŒ Login failed: #{e.message}"
53
+ exit 1
54
+ end
55
+
56
+ private
57
+
58
+ def oidc_flow(client, login_url)
59
+ start = client.start_cli_login
60
+ poll_token = start["poll_token"]
61
+
62
+ open_browser(login_url)
63
+ puts "ā³ Waiting for authentication to complete..."
64
+
65
+ loop do
66
+ res = client.poll_cli_login(poll_token)
67
+
68
+ case res.code.to_i
69
+ when 204
70
+ sleep 2
71
+ next
72
+ when 200
73
+ body = JSON.parse(res.body)
74
+ return {
75
+ token: body["token"],
76
+ user: body["user"]
77
+ }
78
+ when 410
79
+ raise "Login session expired"
80
+ when 404
81
+ raise "Invalid login session"
82
+ else
83
+ raise "Unexpected response: #{res.code}"
84
+ end
85
+ end
86
+ end
87
+
88
+ def password_flow(client)
89
+ email = @email || prompt("Email")
90
+ password = prompt_password("Password")
91
+
92
+ res = client.password_login(email: email, password: password)
93
+
94
+ {
95
+ token: res[:token],
96
+ user: res[:user]
97
+ }
98
+ end
99
+
100
+ def token_flow(client)
101
+ token = ENV["VAULTKIT_TOKEN"] || prompt_password("API Token")
102
+ user = client.whoami(token)
103
+
104
+ {
105
+ token: token,
106
+ user: user
107
+ }
108
+ end
109
+
110
+ def prompt(label)
111
+ print "#{label}: "
112
+ STDIN.gets.strip
113
+ end
114
+
115
+ def prompt_password(label)
116
+ print "#{label}: "
117
+ STDIN.noecho(&:gets).to_s.strip.tap { puts }
118
+ end
119
+
120
+ def open_browser(url)
121
+ os = RbConfig::CONFIG["host_os"]
122
+
123
+ if os =~ /darwin/
124
+ system("open", url)
125
+ elsif os =~ /linux/
126
+ system("xdg-open", url)
127
+ elsif os =~ /mswin|mingw|cygwin/
128
+ system("start", url)
129
+ else
130
+ puts "Open this URL in your browser:\n#{url}"
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,12 @@
1
+ module Vkit
2
+ module CLI
3
+ module Commands
4
+ class LogoutCommand < BaseCommand
5
+ def call
6
+ credential_store.clear_token!
7
+ puts "🧹 Logged out"
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,62 @@
1
+ require "json"
2
+ require_relative "../../policy/bundle_compiler"
3
+
4
+ module Vkit
5
+ module CLI
6
+ module Commands
7
+ class PolicyBundleCommand
8
+ def call(policies_dir:, registry_dir:, out:, org:, version:)
9
+ policies_dir = File.expand_path(policies_dir)
10
+ registry_dir = File.expand_path(registry_dir)
11
+ out = File.expand_path(out)
12
+
13
+ raise "Policies dir not found: #{policies_dir}" unless Dir.exist?(policies_dir)
14
+ raise "Registry dir not found: #{registry_dir}" unless Dir.exist?(registry_dir)
15
+
16
+ version ||= git_sha
17
+
18
+ bundle = Vkit::Policy::BundleCompiler.compile!(
19
+ org_slug: org || "unknown",
20
+ bundle_version: version,
21
+ policies_dir: policies_dir,
22
+ registry_dir: registry_dir,
23
+ source: {
24
+ repo: git_repo,
25
+ ref: git_ref,
26
+ commit_sha: version
27
+ }
28
+ )
29
+
30
+ FileUtils.mkdir_p(File.dirname(out))
31
+ File.write(out, JSON.pretty_generate(bundle))
32
+
33
+ puts "āœ… Policy bundle created"
34
+ puts " Org: #{bundle.dig("bundle", "org_slug")}"
35
+ puts " Version: #{bundle.dig("bundle", "bundle_version")}"
36
+ puts " Checksum: #{bundle.dig("bundle", "checksum")}"
37
+ puts " Output: #{out}"
38
+ end
39
+
40
+ private
41
+
42
+ def git_sha
43
+ `git rev-parse HEAD`.strip
44
+ rescue
45
+ Time.now.to_i.to_s
46
+ end
47
+
48
+ def git_repo
49
+ `git config --get remote.origin.url`.strip
50
+ rescue
51
+ nil
52
+ end
53
+
54
+ def git_ref
55
+ `git rev-parse --abbrev-ref HEAD`.strip
56
+ rescue
57
+ nil
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Vkit
6
+ module CLI
7
+ module Commands
8
+ class PolicyDeployCommand < BaseCommand
9
+ def call(bundle_path:, org:, activate:)
10
+ with_auth do
11
+ bundle_path = File.expand_path(bundle_path)
12
+ raise "Bundle not found: #{bundle_path}" unless File.exist?(bundle_path)
13
+
14
+ bundle = JSON.parse(File.read(bundle_path))
15
+
16
+ response = authenticated_client.post(
17
+ "/api/v1/orgs/#{org}/policy_bundles",
18
+ body: {
19
+ bundle: bundle,
20
+ activate: activate
21
+ }
22
+ )
23
+
24
+ puts "šŸš€ Policy bundle deployed"
25
+ puts " Version: #{response['bundle_version']}"
26
+ puts " State: #{response['state']}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ require_relative "../policy_bundle_validator"
2
+
3
+ module Vkit
4
+ module CLI
5
+ module Commands
6
+ class PolicyValidateCommand
7
+ def call(bundle_path:, schema_path:)
8
+ bundle_path = File.expand_path(bundle_path)
9
+ raise "Bundle not found: #{bundle_path}" unless File.exist?(bundle_path)
10
+
11
+ schema_path ||= default_schema_path
12
+ raise "Schema not found: #{schema_path}" unless File.exist?(schema_path)
13
+
14
+ validator = Vkit::CLI::PolicyBundleValidator.new(
15
+ schema_path: schema_path
16
+ )
17
+
18
+ validator.validate!(bundle_path)
19
+
20
+ puts "āœ… Policy bundle is valid"
21
+ end
22
+
23
+ private
24
+
25
+ def default_schema_path
26
+ File.expand_path("../../policy/schema/policy_bundle.schema.json", __dir__)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require_relative "../../core/table_formatter"
6
+
7
+ module Vkit
8
+ module CLI
9
+ module Commands
10
+ class RequestCommand < BaseCommand
11
+ def call(aql_str, options)
12
+ with_auth do
13
+ user = credential_store.user
14
+ org = user["organization_slug"]
15
+
16
+ aql =
17
+ if aql_str&.strip&.length&.positive?
18
+ JSON.parse(aql_str)
19
+ else
20
+ JSON.parse(STDIN.read)
21
+ end
22
+
23
+ response = authenticated_client.post(
24
+ "/api/v1/orgs/#{org}/requests",
25
+ body: {
26
+ aql: aql,
27
+ options: {
28
+ environment: options[:env],
29
+ requester_region: options[:requester_region],
30
+ dataset_region: options[:dataset_region],
31
+ requester_clearance: options[:requester_clearance],
32
+ datasource: options[:datasource]
33
+ }.compact
34
+ }
35
+ )
36
+
37
+ handle_result(response, options)
38
+ end
39
+ rescue JSON::ParserError
40
+ raise "Invalid JSON AQL"
41
+ end
42
+
43
+ private
44
+
45
+ def handle_result(result, options)
46
+ status = result["status"]
47
+
48
+ if options[:format] == "json"
49
+ puts JSON.pretty_generate(result)
50
+ exit status == "denied" ? 2 : 0
51
+ end
52
+
53
+ case status
54
+ when "denied"
55
+ puts "āŒ DENIED"
56
+ puts "Reason: #{result["reason"]}"
57
+ exit 2
58
+
59
+ when "queued"
60
+ puts "ā³ QUEUED for approval"
61
+ puts "Request ID: #{result["request_id"]}"
62
+ exit 0
63
+
64
+ when "granted"
65
+ expires_at = Time.parse(result["expires_at"]).getlocal
66
+
67
+ puts "āœ… ACCESS GRANTED"
68
+ puts "Grant ID: #{result["grant_id"]}"
69
+ puts "Expires: #{expires_at.strftime("%Y-%m-%d %H:%M:%S %Z")}"
70
+
71
+ if (mask = result["masked_fields"]) && mask.any?
72
+ puts "Masked Fields: #{mask.join(', ')}"
73
+ else
74
+ puts "Masked Fields: none"
75
+ end
76
+
77
+ puts
78
+ puts "To execute and retrieve the data, run:"
79
+ puts " vkit fetch --grant #{result["grant_ref"] || result["grant_id"]}"
80
+ exit 0
81
+
82
+ when "ok"
83
+ rows = result["rows"] || []
84
+
85
+ puts "āœ… OK — #{rows.size} rows"
86
+ puts "ā„¹ļø Query Metadata:"
87
+ puts JSON.pretty_generate(result["meta"] || {})
88
+ puts
89
+ puts "ā„¹ļø Data Rows:"
90
+ Vkit::Core::TableFormatter.render(rows)
91
+ exit 0
92
+
93
+ else
94
+ puts "ā“ Unexpected response:"
95
+ puts result.inspect
96
+ exit 1
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../../core/table_formatter"
5
+
6
+ module Vkit
7
+ module CLI
8
+ module Commands
9
+ class RequestsListCommand < BaseCommand
10
+ def call(state:, format:)
11
+ with_auth do
12
+ user = credential_store.user
13
+ org = user["organization_slug"]
14
+
15
+ params = {}
16
+ params[:state] = state unless state == "all"
17
+
18
+ response = authenticated_client.get(
19
+ "/api/v1/orgs/#{org}/requests",
20
+ params: params
21
+ )
22
+
23
+ render(response, format)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def render(rows, format)
30
+ if rows.empty?
31
+ puts "šŸ“­ No requests found"
32
+ return
33
+ end
34
+
35
+ case format
36
+ when "json"
37
+ puts JSON.pretty_generate(rows)
38
+ when "table"
39
+ Vkit::Core::TableFormatter.render(rows)
40
+ else
41
+ raise "Unknown format: #{format}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Vkit
6
+ module CLI
7
+ module Commands
8
+ class ScanCommand < BaseCommand
9
+ def call(datasource_name, mode: "diff_only")
10
+ with_auth do
11
+ user = credential_store.user
12
+ org = user["organization_slug"]
13
+
14
+ puts "šŸ” Running scan for datasource '#{datasource_name}' (mode=#{mode})..."
15
+
16
+ response = authenticated_client.post(
17
+ "/api/v1/orgs/#{org}/datasources/#{datasource_name}/scan",
18
+ body: {
19
+ datasource: datasource_name,
20
+ mode: mode
21
+ }
22
+ )
23
+
24
+ puts "āœ… Scan completed"
25
+ puts "šŸ†” Scan ID: #{response["scan_id"]}"
26
+ puts "šŸ“Œ Mode: #{response["mode"]}"
27
+
28
+ diff = response["diff"] || {}
29
+
30
+ if diff.empty?
31
+ puts "✨ No changes detected"
32
+ else
33
+ puts "šŸ“ Registry diff:"
34
+ puts "─" * 50
35
+ puts JSON.pretty_generate(diff)
36
+ puts "─" * 50
37
+ end
38
+
39
+ if response.key?("applied")
40
+ puts response["applied"] ? "āœ… Changes applied" : "ā„¹ļø Changes not applied"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,14 @@
1
+ module Vkit
2
+ module CLI
3
+ module Commands
4
+ class WhoamiCommand < BaseCommand
5
+ def call
6
+ with_auth do
7
+ user = credential_store.user
8
+ puts "šŸ‘¤ #{user['email']} (role: #{user['role']}, org: #{user['organization_slug']})"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "commands/base_command"
2
+
3
+ Dir[File.join(__dir__, "commands", "*.rb")].each do |file|
4
+ require_relative file.sub("#{__dir__}/", "")
5
+ end
@@ -0,0 +1,6 @@
1
+ module Vkit
2
+ module CLI
3
+ class Error < StandardError; end
4
+ class ConfigError < Error; end
5
+ end
6
+ end
@@ -0,0 +1,71 @@
1
+ require "json"
2
+ require "json_schemer"
3
+
4
+ module Vkit
5
+ module CLI
6
+ class PolicyBundleValidator
7
+ def initialize(schema_path:)
8
+ @schema = JSON.parse(File.read(schema_path))
9
+ @schemer = JSONSchemer.schema(@schema)
10
+ end
11
+
12
+ def validate!(bundle_path)
13
+ bundle = JSON.parse(File.read(bundle_path))
14
+ errors = @schemer.validate(bundle).to_a
15
+
16
+ return if errors.empty?
17
+
18
+ puts "\nāŒ Policy bundle validation failed\n\n"
19
+
20
+ errors.each do |err|
21
+ puts format_error(err, bundle)
22
+ end
23
+
24
+ puts "\nFix the errors above and re-run validation."
25
+ exit 1
26
+ end
27
+
28
+ private
29
+
30
+ def format_error(err, bundle)
31
+ pointer = err["data_pointer"]
32
+ policy = policy_from_pointer(pointer, bundle)
33
+
34
+ msg = []
35
+ msg << "Policy: #{policy || 'global'}"
36
+ msg << "Path: #{human_path(pointer)}"
37
+ msg << "Error: #{human_message(err)}"
38
+
39
+ msg.join("\n ")
40
+ end
41
+
42
+ def policy_from_pointer(pointer, bundle)
43
+ return nil unless pointer =~ %r{/policies/(\d+)}
44
+
45
+ index = Regexp.last_match(1).to_i
46
+ bundle.dig("policies", index, "id")
47
+ end
48
+
49
+ def human_path(pointer)
50
+ pointer
51
+ .gsub("/", ".")
52
+ .sub(".", "")
53
+ .gsub(/\.(\d+)/, '[\1]')
54
+ end
55
+
56
+ def human_message(err)
57
+ case err["error"]
58
+ when /missing required properties/
59
+ missing = err.dig("details", "missing_keys")&.join(", ")
60
+ "Missing required field(s): #{missing}"
61
+ when /disallowed additional property/
62
+ "This field is not allowed in the compiled policy bundle"
63
+ when /type/
64
+ "Invalid value type"
65
+ else
66
+ err["error"]
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ module Vkit
2
+ module CLI
3
+ class RequestsCLI < Thor
4
+ desc "list", "List your past requests"
5
+ option :state,
6
+ type: :string,
7
+ default: "all",
8
+ enum: %w[all pending approved denied]
9
+
10
+ option :format,
11
+ type: :string,
12
+ default: "table",
13
+ enum: %w[json table]
14
+
15
+ def list
16
+ Commands::RequestsListCommand.new.call(
17
+ state: options[:state],
18
+ format: options[:format]
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
data/lib/vkit/cli.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "thor"
2
+
3
+ require_relative "cli/commands"
4
+ require_relative "cli/base_cli"