vaultkit 1.3.0 → 1.4.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.
- checksums.yaml +4 -4
- data/README.md +20 -0
- data/lib/vkit/cli/base_cli.rb +4 -1
- data/lib/vkit/cli/commands/policy_bundle_command.rb +19 -0
- data/lib/vkit/cli/commands/policy_deploy_command.rb +40 -7
- data/lib/vkit/cli/commands/policy_pack_upgrade_command.rb +43 -1
- data/lib/vkit/cli/commands/policy_validate_command.rb +6 -0
- data/lib/vkit/cli/commands/scan_command.rb +25 -0
- data/lib/vkit/cli/policy_pack/manager.rb +40 -2
- data/lib/vkit/policy/bundle_compiler.rb +7 -21
- data/lib/vkit/policy/pack_integrity.rb +157 -0
- data/lib/vkit/policy/packs/starter/metadata.yaml +2 -2
- data/lib/vkit/policy/packs/starter/policies/06_flag_unknown.yaml +1 -1
- data/lib/vkit/policy/packs/starter/policies/07_mask_unknown_for_agents.yaml +18 -0
- data/lib/vkit/policy/schema/policy_bundle.schema.json +7 -1
- data/lib/vkit/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e2eb23b9fcb727367f905e2d62f8b9de36e23cf9a00f54e067f92d6caff9b282
|
|
4
|
+
data.tar.gz: 8d125c6b810953abd5c0976fe18b2add1c65781431e9a3b2e147d6253fe968bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1780712e8c30407ad5b3319f0df166ba391deff62d79b5e89176867ca4a2d2e8e65b2ec8a4769f28ccd73133bb3e72a87bdf2de3c7ca37ecb79d54ed26ddfba
|
|
7
|
+
data.tar.gz: b6891fd707489ac9ad584459e1a89dcc9eefaa8eb1a73e19b5b93666bceb34c4cbd84246a1b67eede91453c9da94010f875611b35d09e4441dd11695ca93cdf1
|
data/README.md
CHANGED
|
@@ -740,6 +740,26 @@ vkit scan production_pg
|
|
|
740
740
|
|
|
741
741
|
To accept a suggestion, change the field's sensitivity and tags in `datasets/registry.yaml`, then build and deploy the bundle. Control planes that predate suggestions don't send them, and the output is unchanged.
|
|
742
742
|
|
|
743
|
+
**Unreviewed Columns:**
|
|
744
|
+
|
|
745
|
+
Columns the classifier couldn't place keep the `unknown` sensitivity until someone reviews them. The `starter` pack (2.1.0 and later) masks them for agents, so the scan lists them under their dataset, marked `!`, and counts them:
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
vkit scan production_pg
|
|
749
|
+
|
|
750
|
+
# Output:
|
|
751
|
+
# customers ✓
|
|
752
|
+
# ! unreviewed: notes, meta
|
|
753
|
+
#
|
|
754
|
+
# No changes detected
|
|
755
|
+
#
|
|
756
|
+
# ⚠️ 2 columns are unreviewed and masked until you review them
|
|
757
|
+
# (for agents, under the starter pack). To review, set each one's sensitivity
|
|
758
|
+
# in datasets/registry.yaml, then run: vkit policy bundle && vkit policy deploy
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Older control planes don't send unreviewed columns, and the output is unchanged.
|
|
762
|
+
|
|
743
763
|
**Apply Changes:**
|
|
744
764
|
```bash
|
|
745
765
|
vkit scan production_pg --mode apply
|
data/lib/vkit/cli/base_cli.rb
CHANGED
|
@@ -299,11 +299,14 @@ module Vkit
|
|
|
299
299
|
desc "upgrade [NAME]", "Upgrade installed policy packs (all if NAME omitted)"
|
|
300
300
|
option :dir, type: :string, default: "."
|
|
301
301
|
option :force, type: :boolean, default: false
|
|
302
|
+
option :restore, type: :boolean, default: false,
|
|
303
|
+
desc: "Restore an up-to-date pack's missing files without asking"
|
|
302
304
|
def upgrade(name = nil)
|
|
303
305
|
Commands::PolicyPackUpgradeCommand.new.call(
|
|
304
306
|
pack_name: name,
|
|
305
307
|
dir: options[:dir],
|
|
306
|
-
force: options[:force]
|
|
308
|
+
force: options[:force],
|
|
309
|
+
restore: options[:restore]
|
|
307
310
|
)
|
|
308
311
|
end
|
|
309
312
|
}
|
|
@@ -58,6 +58,7 @@ module Vkit
|
|
|
58
58
|
puts " Version: #{bundle.dig("bundle", "bundle_version")}"
|
|
59
59
|
puts " Checksum: #{bundle.dig("bundle", "checksum")}"
|
|
60
60
|
puts " Output: #{out}"
|
|
61
|
+
print_policy_summary(bundle, policies_dir)
|
|
61
62
|
end
|
|
62
63
|
rescue Vkit::Policy::ValidationError => e
|
|
63
64
|
puts e.message
|
|
@@ -66,6 +67,24 @@ module Vkit
|
|
|
66
67
|
|
|
67
68
|
private
|
|
68
69
|
|
|
70
|
+
# Per-pack counts on every bundle, so a pack that contributes nothing
|
|
71
|
+
# is visible even when nothing errors.
|
|
72
|
+
def print_policy_summary(bundle, policies_dir)
|
|
73
|
+
total = bundle["policies"].length
|
|
74
|
+
packs = Array(bundle.dig("bundle", "installed_packs"))
|
|
75
|
+
from_packs = packs.sum { |p| Array(p["policy_ids"]).length }
|
|
76
|
+
rows = packs.map { |p| ["#{p["name"]} #{p["version"]}", Array(p["policy_ids"]).length] }
|
|
77
|
+
rows << ["repo (#{relative(policies_dir)})", total - from_packs]
|
|
78
|
+
width = rows.map { |label, _| label.length }.max
|
|
79
|
+
|
|
80
|
+
puts " Policies: #{total}"
|
|
81
|
+
rows.each { |label, count| puts " #{label.ljust(width)} #{count}" }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def relative(path)
|
|
85
|
+
path.delete_prefix(Dir.pwd + File::SEPARATOR)
|
|
86
|
+
end
|
|
87
|
+
|
|
69
88
|
def git_sha
|
|
70
89
|
out = `git rev-parse HEAD 2>/dev/null`.strip
|
|
71
90
|
return nil if out.empty?
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "digest"
|
|
5
|
+
require_relative "../../policy/pack_integrity"
|
|
6
|
+
require_relative "../../policy/bundle_compiler"
|
|
4
7
|
|
|
5
8
|
module Vkit
|
|
6
9
|
module CLI
|
|
@@ -30,20 +33,50 @@ module Vkit
|
|
|
30
33
|
org_slug = org || derived_org
|
|
31
34
|
|
|
32
35
|
bundle = JSON.parse(File.read(bundle_path))
|
|
36
|
+
Vkit::Policy::PackIntegrity.check_bundle!(bundle)
|
|
33
37
|
|
|
34
|
-
response =
|
|
35
|
-
"/api/v1/orgs/#{org_slug}/policy_bundles",
|
|
36
|
-
body: {
|
|
37
|
-
bundle: bundle,
|
|
38
|
-
activate: activate
|
|
39
|
-
}
|
|
40
|
-
)
|
|
38
|
+
response = upload(org_slug, bundle, activate)
|
|
41
39
|
|
|
42
40
|
puts "🚀 Policy bundle deployed"
|
|
43
41
|
puts " Org: #{org_slug}"
|
|
44
42
|
puts " Version: #{response['bundle_version']}"
|
|
45
43
|
puts " State: #{response['state']}"
|
|
46
44
|
end
|
|
45
|
+
rescue Vkit::Policy::ValidationError => e
|
|
46
|
+
puts e.message
|
|
47
|
+
exit 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# A control plane from before vaultkitcli#47 doesn't know installed_packs'
|
|
53
|
+
# policy_ids and rejects the bundle on schema. The ids were checked
|
|
54
|
+
# above, so send the bundle without them (re-checksummed) instead.
|
|
55
|
+
def upload(org_slug, bundle, activate)
|
|
56
|
+
post_bundle(org_slug, bundle, activate)
|
|
57
|
+
rescue Vkit::CLI::API::APIError => e
|
|
58
|
+
raise unless e.status == 422 && e.error.to_s.match?(%r{installed_packs/\d+/policy_ids})
|
|
59
|
+
|
|
60
|
+
warn "ℹ️ This control plane predates per-pack policy checks; deploying without them."
|
|
61
|
+
post_bundle(org_slug, without_policy_ids(bundle), activate)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def post_bundle(org_slug, bundle, activate)
|
|
65
|
+
authenticated_client.post(
|
|
66
|
+
"/api/v1/orgs/#{org_slug}/policy_bundles",
|
|
67
|
+
body: {
|
|
68
|
+
bundle: bundle,
|
|
69
|
+
activate: activate
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def without_policy_ids(bundle)
|
|
75
|
+
copy = JSON.parse(JSON.generate(bundle))
|
|
76
|
+
Array(copy.dig("bundle", "installed_packs")).each { |p| p.delete("policy_ids") }
|
|
77
|
+
copy["bundle"]["checksum"] = ""
|
|
78
|
+
copy["bundle"]["checksum"] = Digest::SHA256.hexdigest(Vkit::Policy::BundleCompiler.canonical_json(copy))
|
|
79
|
+
copy
|
|
47
80
|
end
|
|
48
81
|
end
|
|
49
82
|
end
|
|
@@ -6,7 +6,9 @@ module Vkit
|
|
|
6
6
|
module CLI
|
|
7
7
|
module Commands
|
|
8
8
|
class PolicyPackUpgradeCommand < BaseCommand
|
|
9
|
-
|
|
9
|
+
# restore: rewrite an up-to-date pack's missing files without asking.
|
|
10
|
+
# Without it, a terminal is asked; anything else is told to rerun.
|
|
11
|
+
def call(pack_name:, dir:, force: false, restore: false, input: $stdin)
|
|
10
12
|
ensure_project!(dir)
|
|
11
13
|
|
|
12
14
|
manager = Vkit::CLI::PolicyPack::Manager.new(
|
|
@@ -22,8 +24,20 @@ module Vkit
|
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
results.each do |r|
|
|
28
|
+
restore_missing(manager, r, restore, input) if r[:status] == :missing
|
|
29
|
+
end
|
|
30
|
+
|
|
25
31
|
print_summary(results)
|
|
26
32
|
|
|
33
|
+
if results.any? { |r| %i[failed missing].include?(r[:status]) }
|
|
34
|
+
if results.any? { |r| r[:status] == :missing }
|
|
35
|
+
puts "\nRestore them with 'vkit policy pack upgrade #{pack_name || "<pack>"} --restore'. " \
|
|
36
|
+
"Until then 'vkit policy bundle' fails."
|
|
37
|
+
end
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
|
|
27
41
|
puts "\nRun 'vkit policy bundle' to recompile."
|
|
28
42
|
rescue StandardError => e
|
|
29
43
|
puts "❌ Error: #{e.message}"
|
|
@@ -41,6 +55,8 @@ module Vkit
|
|
|
41
55
|
def summarize(pack, result)
|
|
42
56
|
if result == :up_to_date
|
|
43
57
|
{ name: pack, status: :up_to_date }
|
|
58
|
+
elsif result[:status] == :missing
|
|
59
|
+
{ name: pack, status: :missing, version: result[:version], files: result[:files] }
|
|
44
60
|
else
|
|
45
61
|
{
|
|
46
62
|
name: pack,
|
|
@@ -52,6 +68,26 @@ module Vkit
|
|
|
52
68
|
end
|
|
53
69
|
end
|
|
54
70
|
|
|
71
|
+
def restore_missing(manager, r, restore, input)
|
|
72
|
+
puts "⚠️ #{r[:name]} #{r[:version]} is installed, but #{r[:files].length} of its files are missing:"
|
|
73
|
+
r[:files].each { |f| puts " #{f["path"]} (#{f["policy_id"]})" }
|
|
74
|
+
|
|
75
|
+
return unless restore || confirm?(input, "Restore them from the pack? [Y/n] ")
|
|
76
|
+
|
|
77
|
+
manager.restore!(r[:name])
|
|
78
|
+
r[:status] = :restored
|
|
79
|
+
rescue => e
|
|
80
|
+
r.merge!(status: :failed, error: e.message)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def confirm?(input, prompt)
|
|
84
|
+
return false unless input.respond_to?(:tty?) && input.tty?
|
|
85
|
+
|
|
86
|
+
print prompt
|
|
87
|
+
answer = input.gets.to_s.strip.downcase
|
|
88
|
+
answer.empty? || answer.start_with?("y")
|
|
89
|
+
end
|
|
90
|
+
|
|
55
91
|
def print_summary(results)
|
|
56
92
|
return if results.empty?
|
|
57
93
|
|
|
@@ -68,6 +104,12 @@ module Vkit
|
|
|
68
104
|
puts " To: v#{r[:new]}"
|
|
69
105
|
puts " Policies updated: #{r[:policies]}"
|
|
70
106
|
|
|
107
|
+
when :restored
|
|
108
|
+
puts "✅ #{r[:name]}: restored #{r[:files].length} missing file(s) at v#{r[:version]}"
|
|
109
|
+
|
|
110
|
+
when :missing
|
|
111
|
+
puts "❌ #{r[:name]}: #{r[:files].length} file(s) missing, not restored"
|
|
112
|
+
|
|
71
113
|
when :failed
|
|
72
114
|
puts "❌ #{r[:name]}: #{r[:error]}"
|
|
73
115
|
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
require "json"
|
|
1
2
|
require_relative "../policy_bundle_validator"
|
|
3
|
+
require_relative "../../policy/pack_integrity"
|
|
2
4
|
|
|
3
5
|
module Vkit
|
|
4
6
|
module CLI
|
|
@@ -16,8 +18,12 @@ module Vkit
|
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
validator.validate!(bundle_path)
|
|
21
|
+
Vkit::Policy::PackIntegrity.check_bundle!(JSON.parse(File.read(bundle_path)))
|
|
19
22
|
|
|
20
23
|
puts "✅ Policy bundle is valid"
|
|
24
|
+
rescue Vkit::Policy::ValidationError => e
|
|
25
|
+
puts e.message
|
|
26
|
+
exit 1
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
private
|
|
@@ -60,6 +60,7 @@ module Vkit
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
print_suggestions(dataset)
|
|
63
|
+
print_unreviewed(dataset)
|
|
63
64
|
|
|
64
65
|
puts
|
|
65
66
|
end
|
|
@@ -78,6 +79,8 @@ module Vkit
|
|
|
78
79
|
puts " datasets/registry.yaml and redeploy the bundle."
|
|
79
80
|
end
|
|
80
81
|
|
|
82
|
+
print_unreviewed_note(datasets)
|
|
83
|
+
|
|
81
84
|
puts
|
|
82
85
|
puts " ✅ Applied" if response["applied"]
|
|
83
86
|
end
|
|
@@ -85,6 +88,28 @@ module Vkit
|
|
|
85
88
|
def print_clean(dataset)
|
|
86
89
|
puts " #{dataset["name"].ljust(24)} ✓"
|
|
87
90
|
print_suggestions(dataset)
|
|
91
|
+
print_unreviewed(dataset)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Columns still labelled unknown. Older control planes don't send them.
|
|
95
|
+
# The starter pack masks them for agents until someone reviews them.
|
|
96
|
+
def print_unreviewed(dataset)
|
|
97
|
+
names = unreviewed_for(dataset)
|
|
98
|
+
puts " ! unreviewed: #{names.join(", ")}" if names.any?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def print_unreviewed_note(datasets)
|
|
102
|
+
count = datasets.sum { |d| unreviewed_for(d).size }
|
|
103
|
+
return if count.zero?
|
|
104
|
+
|
|
105
|
+
puts
|
|
106
|
+
puts " ⚠️ #{count} #{count == 1 ? "column is" : "columns are"} unreviewed and masked until you review them"
|
|
107
|
+
puts " (for agents, under the starter pack). To review, set each one's sensitivity"
|
|
108
|
+
puts " in datasets/registry.yaml, then run: vkit policy bundle && vkit policy deploy"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def unreviewed_for(dataset)
|
|
112
|
+
Array(dataset["unreviewed"])
|
|
88
113
|
end
|
|
89
114
|
|
|
90
115
|
def print_suggestions(dataset)
|
|
@@ -185,10 +185,15 @@ module Vkit
|
|
|
185
185
|
shipped_version = shipped_meta["version"]
|
|
186
186
|
|
|
187
187
|
if installed_version == shipped_version
|
|
188
|
-
|
|
188
|
+
missing = missing_files(pack_name)
|
|
189
|
+
return :up_to_date if missing.empty?
|
|
190
|
+
|
|
191
|
+
return { status: :missing, version: installed_version, files: missing }
|
|
189
192
|
end
|
|
190
193
|
|
|
191
|
-
|
|
194
|
+
# Every file is rewritten at the new version, so old ones that are
|
|
195
|
+
# already gone don't matter.
|
|
196
|
+
remove!(pack_name, force: true) unless dry_run
|
|
192
197
|
|
|
193
198
|
count = install!(pack_name, force: force, dry_run: dry_run)
|
|
194
199
|
|
|
@@ -199,6 +204,39 @@ module Vkit
|
|
|
199
204
|
}
|
|
200
205
|
end
|
|
201
206
|
|
|
207
|
+
# Recorded files of an installed pack that are no longer on disk.
|
|
208
|
+
def missing_files(pack_name)
|
|
209
|
+
raise PackNotInstalled, "Pack '#{pack_name}' is not installed" unless installed?(pack_name)
|
|
210
|
+
|
|
211
|
+
Array(installed_packs[pack_name]["files"]).reject do |f|
|
|
212
|
+
File.exist?(File.expand_path(f.fetch("path"), @project_root))
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Rewrites an installed pack's missing files from the shipped pack,
|
|
217
|
+
# leaving the ones still present (and any edits to them) alone.
|
|
218
|
+
# Returns the restored file entries.
|
|
219
|
+
def restore!(pack_name)
|
|
220
|
+
meta, policies = read_pack!(pack_name)
|
|
221
|
+
installed_version = installed_packs.dig(pack_name, "version")
|
|
222
|
+
|
|
223
|
+
if installed_version != meta["version"]
|
|
224
|
+
raise Error, "Pack '#{pack_name}' #{installed_version} is installed but this CLI ships " \
|
|
225
|
+
"#{meta["version"]}; upgrade it instead"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
by_id = policies.to_h { |p| [p.fetch("id"), p] }
|
|
229
|
+
|
|
230
|
+
missing_files(pack_name).each do |f|
|
|
231
|
+
policy = by_id[f["policy_id"]]
|
|
232
|
+
raise InvalidPack, "Pack '#{pack_name}' no longer ships policy '#{f["policy_id"]}'" unless policy
|
|
233
|
+
|
|
234
|
+
dest = File.expand_path(f.fetch("path"), @project_root)
|
|
235
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
236
|
+
File.write(dest, render_policy_file(pack_name, meta, policy))
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
202
240
|
def upgrade_all!(force: false, dry_run: false)
|
|
203
241
|
# installed packs ordered by layer (low -> high)
|
|
204
242
|
packs = installed_packs.keys.sort_by { |name| safe_pack_metadata(name)["layer"].to_i }
|
|
@@ -6,15 +6,18 @@ require_relative "policy_validator"
|
|
|
6
6
|
require_relative "match_check"
|
|
7
7
|
require_relative "tenancy_check"
|
|
8
8
|
require_relative "validation_error"
|
|
9
|
+
require_relative "pack_integrity"
|
|
9
10
|
|
|
10
11
|
module Vkit
|
|
11
12
|
module Policy
|
|
12
13
|
class BundleCompiler
|
|
13
14
|
FORMAT_VERSION = "v1"
|
|
14
15
|
|
|
15
|
-
# Raises if a policy can never match (see MatchCheck)
|
|
16
|
-
|
|
16
|
+
# Raises if a policy can never match (see MatchCheck), or if an
|
|
17
|
+
# installed pack is missing policies (see PackIntegrity).
|
|
18
|
+
def self.compile!(org_slug:, bundle_version:, policies_dir:, registry_dir:, source: {}, project_root: Dir.pwd)
|
|
17
19
|
policies = load_policies(policies_dir)
|
|
20
|
+
installed_packs = PackIntegrity.check!(project_root: project_root, policies_dir: policies_dir, policies: policies)
|
|
18
21
|
raw_registry = load_raw_registry(registry_dir)
|
|
19
22
|
registry = normalize_registry(raw_registry)
|
|
20
23
|
check_matches!(policies, registry, raw_registry)
|
|
@@ -33,7 +36,7 @@ module Vkit
|
|
|
33
36
|
"signing" => nil
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
bundle["bundle"]["installed_packs"] =
|
|
39
|
+
bundle["bundle"]["installed_packs"] = installed_packs
|
|
37
40
|
canonical = canonical_json(bundle)
|
|
38
41
|
bundle["bundle"]["checksum"] = Digest::SHA256.hexdigest(canonical)
|
|
39
42
|
|
|
@@ -51,7 +54,7 @@ module Vkit
|
|
|
51
54
|
|
|
52
55
|
PolicyValidator.validate!(data, file: File.basename(f))
|
|
53
56
|
|
|
54
|
-
data.merge("__file" => File.basename(f))
|
|
57
|
+
data.merge("__file" => File.basename(f), "__path" => File.expand_path(f))
|
|
55
58
|
end
|
|
56
59
|
end
|
|
57
60
|
|
|
@@ -212,23 +215,6 @@ module Vkit
|
|
|
212
215
|
value
|
|
213
216
|
end
|
|
214
217
|
end
|
|
215
|
-
|
|
216
|
-
def self.load_installed_packs
|
|
217
|
-
tracking_path = File.join(".vkit", "packs.yaml")
|
|
218
|
-
return [] unless File.exist?(tracking_path)
|
|
219
|
-
|
|
220
|
-
data = YAML.safe_load(File.read(tracking_path), permitted_classes: [], permitted_symbols: [], aliases: true)
|
|
221
|
-
packs = data["installed_packs"] || {}
|
|
222
|
-
|
|
223
|
-
packs.map do |name, meta|
|
|
224
|
-
{
|
|
225
|
-
"name" => name,
|
|
226
|
-
"version" => meta["version"]
|
|
227
|
-
}
|
|
228
|
-
end
|
|
229
|
-
rescue
|
|
230
|
-
[]
|
|
231
|
-
end
|
|
232
218
|
end
|
|
233
219
|
end
|
|
234
220
|
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "validation_error"
|
|
5
|
+
|
|
6
|
+
module Vkit
|
|
7
|
+
module Policy
|
|
8
|
+
# Checks that every pack recorded in .vkit/packs.yaml still contributes
|
|
9
|
+
# all of its policies, so a bundle can't quietly lose one (vaultkitcli#47).
|
|
10
|
+
module PackIntegrity
|
|
11
|
+
PACKS_DIR = File.expand_path("packs", __dir__)
|
|
12
|
+
TRACKING_PATH = File.join(".vkit", "packs.yaml")
|
|
13
|
+
VERSION_HEADER = /^# Pack Version: (.+)$/
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Installed packs as recorded, { name => entry }. A missing file means no
|
|
18
|
+
# packs; an unreadable one is an error, since it would hide every pack.
|
|
19
|
+
def installed_packs(project_root)
|
|
20
|
+
path = File.join(project_root, TRACKING_PATH)
|
|
21
|
+
return {} unless File.exist?(path)
|
|
22
|
+
|
|
23
|
+
data = YAML.safe_load(File.read(path), permitted_classes: [], permitted_symbols: [], aliases: true) || {}
|
|
24
|
+
raise ValidationError, "#{TRACKING_PATH} is not a mapping" unless data.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
packs = data["installed_packs"] || {}
|
|
27
|
+
raise ValidationError, "#{TRACKING_PATH}: installed_packs is not a mapping" unless packs.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
packs
|
|
30
|
+
rescue Psych::Exception => e
|
|
31
|
+
raise ValidationError, "Can't read #{TRACKING_PATH}: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Raises unless each installed pack's recorded files are all present in
|
|
35
|
+
# policies_dir, at the recorded version, with the recorded policy ids.
|
|
36
|
+
# `policies` are the loaded policy files, each with "__path".
|
|
37
|
+
# Returns [{ "name", "version", "policy_ids" }] for the bundle.
|
|
38
|
+
def check!(project_root:, policies_dir:, policies:)
|
|
39
|
+
by_path = policies.to_h { |p| [real(p["__path"]), p] }
|
|
40
|
+
problems = []
|
|
41
|
+
|
|
42
|
+
packs = installed_packs(project_root).map do |name, entry|
|
|
43
|
+
entry ||= {}
|
|
44
|
+
version = entry["version"].to_s
|
|
45
|
+
files = Array(entry["files"])
|
|
46
|
+
pack_problems = []
|
|
47
|
+
|
|
48
|
+
pack_problems << "no files recorded in #{TRACKING_PATH}" if files.empty?
|
|
49
|
+
|
|
50
|
+
shipped = shipped_policy_ids(name, version)
|
|
51
|
+
if shipped && files.length < shipped.length
|
|
52
|
+
recorded = files.map { |f| f["policy_id"] }
|
|
53
|
+
pack_problems << "#{TRACKING_PATH} records #{files.length} of the pack's #{shipped.length} policies " \
|
|
54
|
+
"(not recorded: #{(shipped - recorded).join(", ")})"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
files.each do |f|
|
|
58
|
+
rel = f["path"].to_s
|
|
59
|
+
abs = File.expand_path(rel, project_root)
|
|
60
|
+
expected_id = f["policy_id"].to_s
|
|
61
|
+
|
|
62
|
+
unless File.exist?(abs)
|
|
63
|
+
pack_problems << "missing #{rel} (#{expected_id})"
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
policy = by_path[real(abs)]
|
|
68
|
+
unless policy
|
|
69
|
+
pack_problems << "#{rel} (#{expected_id}) is not in the policies directory #{policies_dir}"
|
|
70
|
+
next
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if policy["id"].to_s != expected_id
|
|
74
|
+
pack_problems << "#{rel} has policy id #{policy["id"].inspect}, recorded as #{expected_id.inspect}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
file_version = File.read(abs)[VERSION_HEADER, 1]&.strip&.delete('"')
|
|
78
|
+
if file_version && file_version != version
|
|
79
|
+
pack_problems << "#{rel} is from version #{file_version}, but #{version} is installed"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
problems << [name, version, pack_problems] if pack_problems.any?
|
|
84
|
+
|
|
85
|
+
{ "name" => name.to_s, "version" => version, "policy_ids" => files.map { |f| f["policy_id"].to_s } }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
raise ValidationError, problems_message(problems, restore: true) if problems.any?
|
|
89
|
+
|
|
90
|
+
packs
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Raises unless every policy a bundle's installed_packs lists is in the
|
|
94
|
+
# bundle. Bundles from vkit 1.3.0 and older record only name and
|
|
95
|
+
# version: those are checked against the pack this CLI ships, when it
|
|
96
|
+
# ships that version.
|
|
97
|
+
def check_bundle!(bundle)
|
|
98
|
+
bundled = Array(bundle["policies"]).map { |p| p["id"].to_s }
|
|
99
|
+
problems = []
|
|
100
|
+
|
|
101
|
+
Array(bundle.dig("bundle", "installed_packs")).each do |pack|
|
|
102
|
+
name = pack["name"].to_s
|
|
103
|
+
version = pack["version"].to_s
|
|
104
|
+
expected = pack["policy_ids"] || shipped_policy_ids(name, version)
|
|
105
|
+
next unless expected
|
|
106
|
+
|
|
107
|
+
missing = expected - bundled
|
|
108
|
+
problems << [name, version, ["missing policies: #{missing.join(", ")}"]] if missing.any?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
raise ValidationError, problems_message(problems, restore: false) if problems.any?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def real(path)
|
|
115
|
+
File.realpath(path)
|
|
116
|
+
rescue SystemCallError
|
|
117
|
+
path
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Policy ids of the pack this CLI ships, or nil if it ships another version.
|
|
121
|
+
def shipped_policy_ids(name, version)
|
|
122
|
+
root = File.join(PACKS_DIR, name.to_s)
|
|
123
|
+
meta_path = File.join(root, "metadata.yaml")
|
|
124
|
+
return unless File.exist?(meta_path)
|
|
125
|
+
|
|
126
|
+
meta = (YAML.safe_load(File.read(meta_path), aliases: true) || {})["__pack_meta"] || {}
|
|
127
|
+
return unless meta["version"].to_s == version.to_s
|
|
128
|
+
|
|
129
|
+
Dir.glob(File.join(root, "policies", "*.yaml")).sort.map do |file|
|
|
130
|
+
(YAML.safe_load(File.read(file), aliases: true) || {})["id"].to_s
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def problems_message(problems, restore:)
|
|
135
|
+
lines = problems.map do |name, version, list|
|
|
136
|
+
" #{name} #{version}:\n#{list.map { |p| " - #{p}" }.join("\n")}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
hint =
|
|
140
|
+
if restore
|
|
141
|
+
"Restore the files with `vkit policy pack upgrade <pack>`, or remove the pack with " \
|
|
142
|
+
"`vkit policy pack remove <pack> --force` if you no longer want it."
|
|
143
|
+
else
|
|
144
|
+
"Rebuild the bundle with `vkit policy bundle`, which checks the installed packs."
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
<<~MSG
|
|
148
|
+
❌ Installed policy packs are incomplete; the bundle would leave out their policies:
|
|
149
|
+
|
|
150
|
+
#{lines.join("\n")}
|
|
151
|
+
|
|
152
|
+
#{hint}
|
|
153
|
+
MSG
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
__pack_meta:
|
|
2
2
|
name: starter
|
|
3
|
-
version: "2.
|
|
3
|
+
version: "2.1.0"
|
|
4
4
|
layer: 10
|
|
5
|
-
description: "Secure defaults: secrets denied, PII and high-sensitivity fields masked, approval for high-sensitivity data in production, unreviewed columns flagged."
|
|
5
|
+
description: "Secure defaults: secrets denied, PII and high-sensitivity fields masked, approval for high-sensitivity data in production, unreviewed columns flagged, and masked for agents."
|
|
6
6
|
dependencies: []
|
|
7
7
|
priority_band:
|
|
8
8
|
min: 0
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
id: flag_unknown_sensitivity
|
|
2
|
-
description: Allow columns nobody has reviewed yet, and record why, so they show up in the audit log.
|
|
2
|
+
description: Allow people to read columns nobody has reviewed yet, and record why, so they show up in the audit log. Agents get them masked (mask_unknown_for_agents).
|
|
3
3
|
priority: 20
|
|
4
4
|
|
|
5
5
|
match:
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
id: mask_unknown_for_agents
|
|
2
|
+
description: Mask columns nobody has reviewed yet when an agent reads them, in every environment, until someone gives them a real label.
|
|
3
|
+
priority: 84
|
|
4
|
+
|
|
5
|
+
match:
|
|
6
|
+
fields:
|
|
7
|
+
sensitivity: unknown
|
|
8
|
+
|
|
9
|
+
context:
|
|
10
|
+
requester_role: agent
|
|
11
|
+
|
|
12
|
+
action:
|
|
13
|
+
mask: true
|
|
14
|
+
reason: This column hasn't been reviewed, so it's masked for agents. Give it a sensitivity in datasets/registry.yaml and redeploy the bundle.
|
|
15
|
+
ttl: 1h
|
|
16
|
+
|
|
17
|
+
masking:
|
|
18
|
+
default_method: full
|
|
@@ -78,7 +78,13 @@
|
|
|
78
78
|
"additionalProperties": false,
|
|
79
79
|
"properties": {
|
|
80
80
|
"name": { "type": "string" },
|
|
81
|
-
"version": { "type": "string" }
|
|
81
|
+
"version": { "type": "string" },
|
|
82
|
+
"policy_ids": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"minItems": 1,
|
|
85
|
+
"description": "Ids of the policies this pack contributes. Every one must be in policies.",
|
|
86
|
+
"items": { "type": "string", "minLength": 1 }
|
|
87
|
+
}
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
}
|
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.
|
|
4
|
+
version: 1.4.0
|
|
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-11-
|
|
11
|
+
date: 2026-11-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|
|
@@ -96,6 +96,7 @@ files:
|
|
|
96
96
|
- lib/vkit/core/table_formatter.rb
|
|
97
97
|
- lib/vkit/policy/bundle_compiler.rb
|
|
98
98
|
- lib/vkit/policy/match_check.rb
|
|
99
|
+
- lib/vkit/policy/pack_integrity.rb
|
|
99
100
|
- lib/vkit/policy/packs/ai_safety/metadata.yaml
|
|
100
101
|
- lib/vkit/policy/packs/ai_safety/policies/01_require_approval_agents_sensitive_prod.yaml
|
|
101
102
|
- lib/vkit/policy/packs/ai_safety/policies/02_require_approval_agents_high_sensitivity.yaml
|
|
@@ -110,6 +111,7 @@ files:
|
|
|
110
111
|
- lib/vkit/policy/packs/starter/policies/04_require_approval_high_sensitivity.yaml
|
|
111
112
|
- lib/vkit/policy/packs/starter/policies/05_default_ttl.yaml
|
|
112
113
|
- lib/vkit/policy/packs/starter/policies/06_flag_unknown.yaml
|
|
114
|
+
- lib/vkit/policy/packs/starter/policies/07_mask_unknown_for_agents.yaml
|
|
113
115
|
- lib/vkit/policy/policy_validator.rb
|
|
114
116
|
- lib/vkit/policy/schema/policy_bundle.schema.json
|
|
115
117
|
- lib/vkit/policy/tenancy_check.rb
|