vaultkit 1.0.6 → 1.0.8
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 +4 -4
- data/lib/vkit/cli/base_cli.rb +28 -1
- data/lib/vkit/cli/commands/fetch_command.rb +19 -2
- data/lib/vkit/cli/commands/init_command.rb +15 -5
- data/lib/vkit/cli/commands/policy_bundle_command.rb +10 -11
- data/lib/vkit/cli/commands/registry_diff_command.rb +66 -0
- data/lib/vkit/cli/commands/registry_export_command.rb +86 -0
- data/lib/vkit/cli/commands/request_command.rb +12 -3
- data/lib/vkit/core/ansi.rb +28 -0
- data/lib/vkit/core/registry_diff.rb +93 -0
- data/lib/vkit/core/registry_diff_printer.rb +110 -0
- data/lib/vkit/policy/bundle_compiler.rb +35 -1
- data/lib/vkit/policy/packs/ai_safety/policies/03_mask_sensitive_by_default_for_agents.yaml +3 -0
- data/lib/vkit/policy/packs/financial_compliance/policies/02_mask_payment_tokens.yaml +4 -1
- data/lib/vkit/policy/packs/starter/policies/02_mask_pii_by_default.yaml +7 -4
- data/lib/vkit/policy/policy_validator.rb +73 -3
- data/lib/vkit/policy/schema/policy_bundle.schema.json +9 -13
- data/lib/vkit/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cc5f978b053f8fe4ac4e146a50b90e2fecc93b884073816c760bb0d99502b96
|
|
4
|
+
data.tar.gz: 26b63ecee6b659f0bf0f645455963f712aac27df77850e142dabc894d5b8070a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba3984df812852cab48f7a530d77d021a8ecbb43432cd212f3ed7d0cd14c71705fce6894f05d3b7db84569168cd4004609fe9f9a702ca2b90c6130c59dcf3402
|
|
7
|
+
data.tar.gz: 7d5c1ca12d0fb08798c6c85bfa2fc10fd6db0667129f7d2fa51e72542dae471c734511f3480e1b73ba3c070903985fb5c42bb789905c1dea3aa4e383d3a6b599
|
data/lib/vkit/cli/base_cli.rb
CHANGED
|
@@ -169,13 +169,40 @@ module Vkit
|
|
|
169
169
|
)
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
+
# REGISTRY
|
|
173
|
+
desc "registry SUBCOMMAND ...ARGS", "Manage local registry.yaml"
|
|
174
|
+
subcommand "registry", Class.new(Thor) {
|
|
175
|
+
|
|
176
|
+
desc "export", "Export runtime registry to datasets/registry.yaml"
|
|
177
|
+
option :dir, type: :string, default: "."
|
|
178
|
+
option :out, type: :string, desc: "Custom output path"
|
|
179
|
+
option :force, type: :boolean, default: false
|
|
180
|
+
def export
|
|
181
|
+
Commands::RegistryExportCommand.new.call(
|
|
182
|
+
dir: options[:dir],
|
|
183
|
+
out: options[:out],
|
|
184
|
+
force: options[:force]
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
desc "diff", "Diff local registry.yaml against runtime registry"
|
|
189
|
+
option :dir, type: :string, default: "."
|
|
190
|
+
option :format, type: :string, default: "human", enum: %w[human json]
|
|
191
|
+
def diff
|
|
192
|
+
Commands::RegistryDiffCommand.new.call(
|
|
193
|
+
dir: options[:dir],
|
|
194
|
+
format: options[:format]
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
}
|
|
198
|
+
|
|
172
199
|
# POLICY
|
|
173
200
|
desc "policy SUBCOMMAND ...ARGS", "Manage policy bundles"
|
|
174
201
|
subcommand "policy", Class.new(Thor) {
|
|
175
202
|
|
|
176
203
|
desc "bundle", "Compile YAML policies into a JSON policy bundle"
|
|
177
204
|
option :policies_dir, type: :string, default: "config/policies"
|
|
178
|
-
option :registry_dir, type: :string, default: "
|
|
205
|
+
option :registry_dir, type: :string, default: "datasets"
|
|
179
206
|
option :out, type: :string, default: "dist/policy_bundle.json"
|
|
180
207
|
option :org, type: :string
|
|
181
208
|
option :version, type: :string
|
|
@@ -16,8 +16,7 @@ module Vkit
|
|
|
16
16
|
body: {}
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
-
rows = response
|
|
20
|
-
meta = response.dig("rows", "meta") || {}
|
|
19
|
+
rows, meta = normalize_response(response)
|
|
21
20
|
|
|
22
21
|
print_result(rows, meta, format)
|
|
23
22
|
end
|
|
@@ -42,6 +41,24 @@ module Vkit
|
|
|
42
41
|
raise "Unknown format: #{format}"
|
|
43
42
|
end
|
|
44
43
|
end
|
|
44
|
+
|
|
45
|
+
def normalize_response(response)
|
|
46
|
+
rows =
|
|
47
|
+
if response["rows"].is_a?(Array)
|
|
48
|
+
response["rows"]
|
|
49
|
+
else
|
|
50
|
+
response.dig("rows", "rows") || []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
meta =
|
|
54
|
+
if response["meta"].is_a?(Hash)
|
|
55
|
+
response["meta"]
|
|
56
|
+
else
|
|
57
|
+
response.dig("rows", "meta") || {}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
[rows, meta]
|
|
61
|
+
end
|
|
45
62
|
end
|
|
46
63
|
end
|
|
47
64
|
end
|
|
@@ -59,15 +59,25 @@ module Vkit
|
|
|
59
59
|
FileUtils.mkdir_p(File.join(dir, "datasets"))
|
|
60
60
|
FileUtils.mkdir_p(File.join(dir, "dist"))
|
|
61
61
|
FileUtils.mkdir_p(File.join(dir, ".vkit"))
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
registry_path = File.join(dir, "datasets", "registry.yaml")
|
|
64
|
-
|
|
64
|
+
|
|
65
|
+
if File.exist?(registry_path)
|
|
66
|
+
puts "ℹ️ datasets/registry.yaml already exists — leaving unchanged."
|
|
67
|
+
else
|
|
65
68
|
File.write(
|
|
66
69
|
registry_path,
|
|
67
|
-
|
|
70
|
+
<<~YAML
|
|
71
|
+
# VaultKit Dataset Registry
|
|
72
|
+
#
|
|
73
|
+
# Populate with:
|
|
74
|
+
# vkit scan <datasource> --apply
|
|
75
|
+
# vkit registry export
|
|
76
|
+
#
|
|
77
|
+
YAML
|
|
68
78
|
)
|
|
69
79
|
end
|
|
70
|
-
|
|
80
|
+
|
|
71
81
|
gitignore_path = File.join(dir, ".gitignore")
|
|
72
82
|
unless File.exist?(gitignore_path)
|
|
73
83
|
File.write(
|
|
@@ -75,7 +85,7 @@ module Vkit
|
|
|
75
85
|
"dist/\n.vkit/\n"
|
|
76
86
|
)
|
|
77
87
|
end
|
|
78
|
-
end
|
|
88
|
+
end
|
|
79
89
|
end
|
|
80
90
|
end
|
|
81
91
|
end
|
|
@@ -67,22 +67,21 @@ module Vkit
|
|
|
67
67
|
private
|
|
68
68
|
|
|
69
69
|
def git_sha
|
|
70
|
-
`git rev-parse HEAD`.strip
|
|
71
|
-
|
|
70
|
+
out = `git rev-parse HEAD 2>/dev/null`.strip
|
|
71
|
+
return out unless out.empty?
|
|
72
|
+
|
|
72
73
|
Time.now.to_i.to_s
|
|
73
74
|
end
|
|
74
|
-
|
|
75
|
+
|
|
75
76
|
def git_repo
|
|
76
|
-
`git config --get remote.origin.url`.strip
|
|
77
|
-
|
|
78
|
-
nil
|
|
77
|
+
out = `git config --get remote.origin.url 2>/dev/null`.strip
|
|
78
|
+
out.empty? ? nil : out
|
|
79
79
|
end
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
def git_ref
|
|
82
|
-
`git rev-parse --abbrev-ref HEAD`.strip
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
end
|
|
82
|
+
out = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
83
|
+
out.empty? ? nil : out
|
|
84
|
+
end
|
|
86
85
|
end
|
|
87
86
|
end
|
|
88
87
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "../../core/registry_diff"
|
|
6
|
+
require_relative "../../core/registry_diff_printer"
|
|
7
|
+
|
|
8
|
+
module Vkit
|
|
9
|
+
module CLI
|
|
10
|
+
module Commands
|
|
11
|
+
class RegistryDiffCommand < BaseCommand
|
|
12
|
+
DEFAULT_PATH = File.join("datasets", "registry.yaml")
|
|
13
|
+
|
|
14
|
+
def call(dir:, format: "human")
|
|
15
|
+
ensure_project!(dir)
|
|
16
|
+
|
|
17
|
+
dir = File.expand_path(dir)
|
|
18
|
+
path = File.join(dir, DEFAULT_PATH)
|
|
19
|
+
|
|
20
|
+
unless File.exist?(path)
|
|
21
|
+
puts "❌ No local registry.yaml found."
|
|
22
|
+
exit 2
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
with_auth do
|
|
26
|
+
user = credential_store.user
|
|
27
|
+
org = user["organization_slug"]
|
|
28
|
+
|
|
29
|
+
local = YAML.safe_load(
|
|
30
|
+
File.read(path),
|
|
31
|
+
permitted_classes: [],
|
|
32
|
+
permitted_symbols: [],
|
|
33
|
+
aliases: false
|
|
34
|
+
)
|
|
35
|
+
runtime = authenticated_client.get(
|
|
36
|
+
"/api/v1/orgs/#{org}/registries/export"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
diff = Vkit::Core::RegistryDiff.compute(
|
|
40
|
+
local: local,
|
|
41
|
+
remote: runtime
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if format == "json"
|
|
45
|
+
puts JSON.pretty_generate(diff)
|
|
46
|
+
exit diff["datasets"].empty? ? 0 : 1
|
|
47
|
+
else
|
|
48
|
+
Vkit::Core::RegistryDiffPrinter.print(diff)
|
|
49
|
+
exit diff["datasets"].empty? ? 0 : 1
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
puts "❌ Error: #{e.message}"
|
|
55
|
+
exit 2
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def ensure_project!(dir)
|
|
61
|
+
raise "Not a VaultKit project (missing .vkit/)" unless Dir.exist?(File.join(dir, ".vkit"))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "base_command"
|
|
5
|
+
|
|
6
|
+
module Vkit
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class RegistryExportCommand < BaseCommand
|
|
10
|
+
DEFAULT_PATH = File.join("datasets", "registry.yaml")
|
|
11
|
+
|
|
12
|
+
def call(dir:, out: nil, force: false)
|
|
13
|
+
with_auth do
|
|
14
|
+
ensure_project!(dir)
|
|
15
|
+
|
|
16
|
+
dir = File.expand_path(dir)
|
|
17
|
+
path = out ? File.expand_path(out, dir) : File.join(dir, DEFAULT_PATH)
|
|
18
|
+
|
|
19
|
+
if File.exist?(path) && !force
|
|
20
|
+
decision = handle_existing_registry(dir: dir, path: path)
|
|
21
|
+
return if decision == :abort
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
user = credential_store.user
|
|
25
|
+
org = user["organization_slug"]
|
|
26
|
+
|
|
27
|
+
registry_data = authenticated_client.get(
|
|
28
|
+
"/api/v1/orgs/#{org}/registries/export"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
32
|
+
File.write(path, registry_data.to_yaml)
|
|
33
|
+
|
|
34
|
+
puts "✅ Registry exported to:"
|
|
35
|
+
puts " #{relative(dir, path)}"
|
|
36
|
+
end
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
puts "❌ Error: #{e.message}"
|
|
39
|
+
exit 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def ensure_project!(dir)
|
|
45
|
+
raise "Not a VaultKit project (missing .vkit/)" unless Dir.exist?(File.join(dir, ".vkit"))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def handle_existing_registry(dir:, path:)
|
|
49
|
+
puts "\n⚠️ Existing registry.yaml detected at:"
|
|
50
|
+
puts " #{relative(dir, path)}\n\n"
|
|
51
|
+
|
|
52
|
+
puts "Choose an option:"
|
|
53
|
+
puts " 1) Overwrite entirely"
|
|
54
|
+
puts " 2) Show diff"
|
|
55
|
+
puts " 3) Abort"
|
|
56
|
+
print "\nEnter choice [1-3]: "
|
|
57
|
+
|
|
58
|
+
choice = STDIN.gets&.strip
|
|
59
|
+
|
|
60
|
+
case choice
|
|
61
|
+
when "1"
|
|
62
|
+
puts "\nOverwriting existing registry..."
|
|
63
|
+
:overwrite
|
|
64
|
+
|
|
65
|
+
when "2"
|
|
66
|
+
show_diff(dir: dir)
|
|
67
|
+
puts "\nAborting."
|
|
68
|
+
:abort
|
|
69
|
+
|
|
70
|
+
else
|
|
71
|
+
puts "\nAborting."
|
|
72
|
+
:abort
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def relative(root, abs)
|
|
77
|
+
abs.sub(root + File::SEPARATOR, "")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def show_diff(dir:)
|
|
81
|
+
Vkit::CLI::Commands::RegistryDiffCommand.new.call(dir: dir)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -80,11 +80,20 @@ module Vkit
|
|
|
80
80
|
exit 0
|
|
81
81
|
|
|
82
82
|
when "ok"
|
|
83
|
-
rows = result["rows"]
|
|
83
|
+
rows = result["rows"]
|
|
84
|
+
meta = result["meta"] || {}
|
|
85
|
+
|
|
86
|
+
unless rows.is_a?(Array)
|
|
87
|
+
raise "Invalid response: expected rows to be an array"
|
|
88
|
+
end
|
|
84
89
|
|
|
85
90
|
puts "✅ OK — #{rows.size} rows"
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
|
|
92
|
+
if meta.any?
|
|
93
|
+
puts "ℹ️ Query Metadata:"
|
|
94
|
+
puts JSON.pretty_generate(meta)
|
|
95
|
+
end
|
|
96
|
+
|
|
88
97
|
puts
|
|
89
98
|
puts "ℹ️ Data Rows:"
|
|
90
99
|
Vkit::Core::TableFormatter.render(rows)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vkit
|
|
4
|
+
module Core
|
|
5
|
+
module Ansi
|
|
6
|
+
COLORS = {
|
|
7
|
+
red: 31,
|
|
8
|
+
green: 32,
|
|
9
|
+
yellow: 33,
|
|
10
|
+
blue: 34,
|
|
11
|
+
gray: 90
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def self.color(text, color)
|
|
15
|
+
return text unless $stdout.tty?
|
|
16
|
+
|
|
17
|
+
code = COLORS[color]
|
|
18
|
+
"\e[#{code}m#{text}\e[0m"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.green(text) = color(text, :green)
|
|
22
|
+
def self.red(text) = color(text, :red)
|
|
23
|
+
def self.yellow(text) = color(text, :yellow)
|
|
24
|
+
def self.blue(text) = color(text, :blue)
|
|
25
|
+
def self.gray(text) = color(text, :gray)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vkit
|
|
4
|
+
module Core
|
|
5
|
+
class RegistryDiff
|
|
6
|
+
def self.compute(local:, remote:)
|
|
7
|
+
local ||= {}
|
|
8
|
+
remote ||= {}
|
|
9
|
+
|
|
10
|
+
local_datasets = normalize(local)
|
|
11
|
+
remote_datasets = normalize(remote)
|
|
12
|
+
|
|
13
|
+
out = { "datasets" => [] }
|
|
14
|
+
|
|
15
|
+
all_names = (local_datasets.keys | remote_datasets.keys).sort
|
|
16
|
+
|
|
17
|
+
all_names.each do |name|
|
|
18
|
+
local_fields = local_datasets[name] || []
|
|
19
|
+
remote_fields = remote_datasets[name] || []
|
|
20
|
+
|
|
21
|
+
changes = diff_fields(local_fields, remote_fields)
|
|
22
|
+
|
|
23
|
+
next if changes.values.all?(&:empty?)
|
|
24
|
+
|
|
25
|
+
out["datasets"] << {
|
|
26
|
+
"name" => name,
|
|
27
|
+
"changes" => changes
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
out
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.normalize(registry)
|
|
35
|
+
return {} unless registry.is_a?(Hash)
|
|
36
|
+
|
|
37
|
+
# CASE 1: runtime export format
|
|
38
|
+
if registry.key?("datasets")
|
|
39
|
+
return Array(registry["datasets"]).each_with_object({}) do |ds, h|
|
|
40
|
+
h[ds["name"]] =
|
|
41
|
+
Array(ds["fields"]).map do |f|
|
|
42
|
+
{
|
|
43
|
+
"name" => f["name"].to_s,
|
|
44
|
+
"type" => f["type"].to_s,
|
|
45
|
+
"sensitivity" => f["sensitivity"].to_s,
|
|
46
|
+
"tags" => Array(f["tags"]).map(&:to_s).sort
|
|
47
|
+
}
|
|
48
|
+
end.sort_by { |f| f["name"] }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# CASE 2: local YAML format
|
|
53
|
+
registry.each_with_object({}) do |(dataset_name, ds), h|
|
|
54
|
+
fields = ds["fields"] || {}
|
|
55
|
+
|
|
56
|
+
normalized_fields =
|
|
57
|
+
fields.map do |field_name, meta|
|
|
58
|
+
{
|
|
59
|
+
"name" => field_name.to_s,
|
|
60
|
+
"type" => meta["type"].to_s,
|
|
61
|
+
"sensitivity" => meta["sensitivity"].to_s,
|
|
62
|
+
"tags" => [meta["category"]].compact.map(&:to_s).sort
|
|
63
|
+
}
|
|
64
|
+
end.sort_by { |f| f["name"] }
|
|
65
|
+
|
|
66
|
+
h[dataset_name.to_s] = normalized_fields
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def self.diff_fields(local, remote)
|
|
72
|
+
l = local.each_with_object({}) { |f, h| h[f["name"]] = f }
|
|
73
|
+
r = remote.each_with_object({}) { |f, h| h[f["name"]] = f }
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
"added_fields" =>
|
|
77
|
+
(r.keys - l.keys).map { |k| r[k] },
|
|
78
|
+
|
|
79
|
+
"removed_fields" =>
|
|
80
|
+
(l.keys - r.keys).map { |k| l[k] },
|
|
81
|
+
|
|
82
|
+
"changed_fields" =>
|
|
83
|
+
(l.keys & r.keys).filter_map do |k|
|
|
84
|
+
next if l[k] == r[k]
|
|
85
|
+
{ "name" => k, "local" => l[k], "remote" => r[k] }
|
|
86
|
+
end
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private_class_method :diff_fields, :normalize
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ansi"
|
|
4
|
+
|
|
5
|
+
module Vkit
|
|
6
|
+
module Core
|
|
7
|
+
class RegistryDiffPrinter
|
|
8
|
+
def self.print(diff)
|
|
9
|
+
datasets = diff["datasets"] || []
|
|
10
|
+
|
|
11
|
+
summary = summarize(datasets)
|
|
12
|
+
|
|
13
|
+
if summary[:total_changes].zero?
|
|
14
|
+
puts Ansi.green("✓ No differences between local registry and runtime.")
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
print_summary(summary)
|
|
19
|
+
puts
|
|
20
|
+
|
|
21
|
+
datasets.each do |ds|
|
|
22
|
+
changes = ds["changes"]
|
|
23
|
+
next if changes.values.all?(&:empty?)
|
|
24
|
+
|
|
25
|
+
puts Ansi.blue("Dataset: #{ds['name']}")
|
|
26
|
+
puts
|
|
27
|
+
|
|
28
|
+
print_added(changes["added_fields"])
|
|
29
|
+
print_removed(changes["removed_fields"])
|
|
30
|
+
print_changed(changes["changed_fields"])
|
|
31
|
+
|
|
32
|
+
puts "-" * 50
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.summarize(datasets)
|
|
37
|
+
datasets.each_with_object(
|
|
38
|
+
datasets_changed: 0,
|
|
39
|
+
added: 0,
|
|
40
|
+
removed: 0,
|
|
41
|
+
changed: 0,
|
|
42
|
+
total_changes: 0
|
|
43
|
+
) do |ds, acc|
|
|
44
|
+
changes = ds["changes"] || {}
|
|
45
|
+
|
|
46
|
+
a = Array(changes["added_fields"]).size
|
|
47
|
+
r = Array(changes["removed_fields"]).size
|
|
48
|
+
c = Array(changes["changed_fields"]).size
|
|
49
|
+
|
|
50
|
+
next if a + r + c == 0
|
|
51
|
+
|
|
52
|
+
acc[:datasets_changed] += 1
|
|
53
|
+
acc[:added] += a
|
|
54
|
+
acc[:removed] += r
|
|
55
|
+
acc[:changed] += c
|
|
56
|
+
acc[:total_changes] += (a + r + c)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.print_summary(summary)
|
|
61
|
+
line = [
|
|
62
|
+
"#{summary[:datasets_changed]} datasets changed",
|
|
63
|
+
"#{summary[:added]} added",
|
|
64
|
+
"#{summary[:removed]} removed",
|
|
65
|
+
"#{summary[:changed]} modified"
|
|
66
|
+
].join(" | ")
|
|
67
|
+
|
|
68
|
+
puts Ansi.yellow(line)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.print_added(fields)
|
|
72
|
+
return if fields.empty?
|
|
73
|
+
|
|
74
|
+
puts Ansi.green(" + Added Fields:")
|
|
75
|
+
fields.each do |f|
|
|
76
|
+
puts Ansi.green(" + #{f['name']} (#{f['type']})")
|
|
77
|
+
end
|
|
78
|
+
puts
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.print_removed(fields)
|
|
82
|
+
return if fields.empty?
|
|
83
|
+
|
|
84
|
+
puts Ansi.red(" - Removed Fields:")
|
|
85
|
+
fields.each do |f|
|
|
86
|
+
puts Ansi.red(" - #{f['name']} (#{f['type']})")
|
|
87
|
+
end
|
|
88
|
+
puts
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.print_changed(fields)
|
|
92
|
+
return if fields.empty?
|
|
93
|
+
|
|
94
|
+
puts Ansi.yellow(" ~ Changed Fields:")
|
|
95
|
+
fields.each do |f|
|
|
96
|
+
puts Ansi.yellow(" ~ #{f['name']}")
|
|
97
|
+
puts Ansi.gray(" From: #{f['from']}")
|
|
98
|
+
puts Ansi.gray(" To: #{f['to']}")
|
|
99
|
+
end
|
|
100
|
+
puts
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private_class_method :summarize,
|
|
104
|
+
:print_summary,
|
|
105
|
+
:print_added,
|
|
106
|
+
:print_removed,
|
|
107
|
+
:print_changed
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -104,7 +104,31 @@ module Vkit
|
|
|
104
104
|
|
|
105
105
|
def self.extract_masking(p)
|
|
106
106
|
return unless p.dig("action", "mask")
|
|
107
|
-
|
|
107
|
+
|
|
108
|
+
raw =
|
|
109
|
+
p["masking"] ||
|
|
110
|
+
p.dig("action", "masking") || {}
|
|
111
|
+
|
|
112
|
+
default_method =
|
|
113
|
+
normalize_mask_method(raw["default_method"]) if raw["default_method"]
|
|
114
|
+
|
|
115
|
+
rules =
|
|
116
|
+
case raw["rules"]
|
|
117
|
+
when Hash
|
|
118
|
+
raw["rules"].each_with_object({}) do |(field, method), acc|
|
|
119
|
+
acc[field.to_s] = normalize_mask_method(method)
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
{}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
result = {}
|
|
126
|
+
result["default_method"] = default_method if default_method
|
|
127
|
+
result["rules"] = rules if rules.any?
|
|
128
|
+
|
|
129
|
+
return if result.empty?
|
|
130
|
+
|
|
131
|
+
result
|
|
108
132
|
end
|
|
109
133
|
|
|
110
134
|
def self.normalize_registry(raw)
|
|
@@ -139,6 +163,16 @@ module Vkit
|
|
|
139
163
|
end
|
|
140
164
|
end
|
|
141
165
|
|
|
166
|
+
def self.normalize_mask_method(method)
|
|
167
|
+
case method.to_s
|
|
168
|
+
when "redact" then "full"
|
|
169
|
+
when "hash" then "hash"
|
|
170
|
+
when "truncate" then "partial"
|
|
171
|
+
when "nullify" then "full"
|
|
172
|
+
else "full"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
142
176
|
# Canonicalization
|
|
143
177
|
def self.canonical_json(obj)
|
|
144
178
|
JSON.generate(sort_keys_deep(obj))
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
id: mask_pii_by_default
|
|
2
2
|
description: "Mask PII fields by default unless explicitly allowed."
|
|
3
3
|
|
|
4
|
+
priority: 80
|
|
5
|
+
|
|
4
6
|
match:
|
|
5
7
|
fields:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
priority: 80
|
|
8
|
+
contains: ["pii"]
|
|
9
9
|
|
|
10
10
|
action:
|
|
11
11
|
mask: true
|
|
12
12
|
reason: "PII is masked by default for safety."
|
|
13
|
-
ttl:
|
|
13
|
+
ttl: 1h
|
|
14
|
+
|
|
15
|
+
masking:
|
|
16
|
+
default_method: redact
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Vkit
|
|
2
4
|
module Policy
|
|
3
5
|
class PolicyValidator
|
|
@@ -28,7 +30,7 @@ module Vkit
|
|
|
28
30
|
MSG
|
|
29
31
|
end
|
|
30
32
|
|
|
31
|
-
validate_action!(action, prefix)
|
|
33
|
+
validate_action!(action, policy, prefix)
|
|
32
34
|
true
|
|
33
35
|
end
|
|
34
36
|
|
|
@@ -41,7 +43,7 @@ module Vkit
|
|
|
41
43
|
MSG
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
def self.validate_action!(action, prefix)
|
|
46
|
+
def self.validate_action!(action, policy, prefix)
|
|
45
47
|
intents = ACTION_KEYS.select { |k| action[k] == true }
|
|
46
48
|
|
|
47
49
|
if intents.empty?
|
|
@@ -81,7 +83,8 @@ module Vkit
|
|
|
81
83
|
require_string!(action, "approver_role", prefix)
|
|
82
84
|
|
|
83
85
|
when "mask"
|
|
84
|
-
# masking
|
|
86
|
+
# masking is optional, but if present must be valid
|
|
87
|
+
validate_masking!(policy, prefix) if policy.key?("masking")
|
|
85
88
|
|
|
86
89
|
when "allow"
|
|
87
90
|
# nothing required
|
|
@@ -124,6 +127,73 @@ module Vkit
|
|
|
124
127
|
MSG
|
|
125
128
|
end
|
|
126
129
|
end
|
|
130
|
+
|
|
131
|
+
# Masking validation (aligned with compiler + runtime)
|
|
132
|
+
def self.validate_masking!(policy, prefix)
|
|
133
|
+
masking = policy["masking"]
|
|
134
|
+
return unless masking
|
|
135
|
+
|
|
136
|
+
unless masking.is_a?(Hash)
|
|
137
|
+
raise ValidationError, "#{prefix}masking must be a mapping."
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
allowed_keys = %w[default_method rules]
|
|
141
|
+
unknown_keys = masking.keys - allowed_keys
|
|
142
|
+
|
|
143
|
+
if unknown_keys.any?
|
|
144
|
+
raise ValidationError, <<~MSG
|
|
145
|
+
#{prefix}Unknown keys in masking: #{unknown_keys.join(', ')}
|
|
146
|
+
|
|
147
|
+
Allowed keys:
|
|
148
|
+
- default_method
|
|
149
|
+
- rules
|
|
150
|
+
MSG
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if masking["default_method"]
|
|
154
|
+
validate_mask_method!(
|
|
155
|
+
masking["default_method"],
|
|
156
|
+
prefix,
|
|
157
|
+
"masking.default_method"
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if masking["rules"]
|
|
162
|
+
unless masking["rules"].is_a?(Hash)
|
|
163
|
+
raise ValidationError,
|
|
164
|
+
"#{prefix}masking.rules must be a mapping of field → method."
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
masking["rules"].each do |field, method|
|
|
168
|
+
validate_mask_method!(
|
|
169
|
+
method,
|
|
170
|
+
prefix,
|
|
171
|
+
"masking.rules.#{field}"
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def self.validate_mask_method!(method, prefix, path)
|
|
178
|
+
allowed = %w[redact hash truncate nullify full partial]
|
|
179
|
+
|
|
180
|
+
unless allowed.include?(method.to_s)
|
|
181
|
+
raise ValidationError, <<~MSG
|
|
182
|
+
#{prefix}Invalid masking method at #{path}.
|
|
183
|
+
|
|
184
|
+
Allowed values:
|
|
185
|
+
- redact
|
|
186
|
+
- hash
|
|
187
|
+
- truncate
|
|
188
|
+
- nullify
|
|
189
|
+
- full
|
|
190
|
+
- partial
|
|
191
|
+
|
|
192
|
+
Got:
|
|
193
|
+
#{method.inspect}
|
|
194
|
+
MSG
|
|
195
|
+
end
|
|
196
|
+
end
|
|
127
197
|
end
|
|
128
198
|
end
|
|
129
199
|
end
|
|
@@ -264,19 +264,15 @@
|
|
|
264
264
|
"type": "object",
|
|
265
265
|
"additionalProperties": false,
|
|
266
266
|
"properties": {
|
|
267
|
-
"
|
|
268
|
-
"type": "
|
|
269
|
-
"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"type": "string",
|
|
277
|
-
"enum": ["redact", "hash", "truncate", "nullify"]
|
|
278
|
-
}
|
|
279
|
-
}
|
|
267
|
+
"default_method": {
|
|
268
|
+
"type": "string",
|
|
269
|
+
"enum": ["full", "hash", "partial"]
|
|
270
|
+
},
|
|
271
|
+
"rules": {
|
|
272
|
+
"type": "object",
|
|
273
|
+
"additionalProperties": {
|
|
274
|
+
"type": "string",
|
|
275
|
+
"enum": ["full", "hash", "partial"]
|
|
280
276
|
}
|
|
281
277
|
}
|
|
282
278
|
}
|
data/lib/vkit/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vaultkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nnamdi Ogundu
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|
|
@@ -61,6 +61,8 @@ files:
|
|
|
61
61
|
- lib/vkit/cli/commands/policy_pack_upgrade_command.rb
|
|
62
62
|
- lib/vkit/cli/commands/policy_revoke_command.rb
|
|
63
63
|
- lib/vkit/cli/commands/policy_validate_command.rb
|
|
64
|
+
- lib/vkit/cli/commands/registry_diff_command.rb
|
|
65
|
+
- lib/vkit/cli/commands/registry_export_command.rb
|
|
64
66
|
- lib/vkit/cli/commands/request_command.rb
|
|
65
67
|
- lib/vkit/cli/commands/requests_list_command.rb
|
|
66
68
|
- lib/vkit/cli/commands/reset_command.rb
|
|
@@ -70,9 +72,12 @@ files:
|
|
|
70
72
|
- lib/vkit/cli/policy_bundle_validator.rb
|
|
71
73
|
- lib/vkit/cli/policy_pack/manager.rb
|
|
72
74
|
- lib/vkit/cli/requests_cli.rb
|
|
75
|
+
- lib/vkit/core/ansi.rb
|
|
73
76
|
- lib/vkit/core/auth_client.rb
|
|
74
77
|
- lib/vkit/core/credential_resolver.rb
|
|
75
78
|
- lib/vkit/core/credential_store.rb
|
|
79
|
+
- lib/vkit/core/registry_diff.rb
|
|
80
|
+
- lib/vkit/core/registry_diff_printer.rb
|
|
76
81
|
- lib/vkit/core/table_formatter.rb
|
|
77
82
|
- lib/vkit/policy/bundle_compiler.rb
|
|
78
83
|
- lib/vkit/policy/packs/ai_safety/metadata.yaml
|