Class: Vkit::Policy::BundleCompiler

Inherits:
Object
  • Object
show all
Defined in:
lib/vkit/policy/bundle_compiler.rb

Constant Summary collapse

FORMAT_VERSION =
"v1"

Class Method Summary collapse

Class Method Details

.canonical_json(obj) ⇒ Object

Canonicalization



177
178
179
# File 'lib/vkit/policy/bundle_compiler.rb', line 177

def self.canonical_json(obj)
  JSON.generate(sort_keys_deep(obj))
end

.compile!(org_slug:, bundle_version:, policies_dir:, registry_dir:, source: {}) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/vkit/policy/bundle_compiler.rb', line 12

def self.compile!(org_slug:, bundle_version:, policies_dir:, registry_dir:, source: {})
  policies = load_policies(policies_dir)
  registry = load_registry(registry_dir)

  bundle = {
    "bundle" => {
      "format_version" => FORMAT_VERSION,
      "org_slug" => org_slug,
      "bundle_version" => bundle_version,
      "created_at" => Time.now.utc.iso8601,
      "source" => normalize_source(source),
      "checksum" => "" # filled below
    },
    "registry" => registry,
    "policies" => normalize_policies(policies),
    "signing" => nil
  }

  bundle["bundle"]["installed_packs"] = load_installed_packs
  canonical = canonical_json(bundle)
  bundle["bundle"]["checksum"] = Digest::SHA256.hexdigest(canonical)

  bundle
end

.extract_approval(p) ⇒ Object



100
101
102
103
# File 'lib/vkit/policy/bundle_compiler.rb', line 100

def self.extract_approval(p)
  return unless p.dig("action", "require_approval")
  { "approver_role" => p.dig("action", "approver_role") }
end

.extract_masking(p) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/vkit/policy/bundle_compiler.rb', line 105

def self.extract_masking(p)
  return unless p.dig("action", "mask")

  raw =
    p["masking"] ||
    p.dig("action", "masking") || {}

  default_method =
    normalize_mask_method(raw["default_method"]) if raw["default_method"]

  rules =
    case raw["rules"]
    when Hash
      raw["rules"].each_with_object({}) do |(field, method), acc|
        acc[field.to_s] = normalize_mask_method(method)
      end
    else
      {}
    end

  result = {}
  result["default_method"] = default_method if default_method
  result["rules"] = rules if rules.any?

  return if result.empty?

  result
end

.load_installed_packsObject



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/vkit/policy/bundle_compiler.rb', line 192

def self.load_installed_packs
  tracking_path = File.join(".vkit", "packs.yaml")
  return [] unless File.exist?(tracking_path)

  data = YAML.safe_load(File.read(tracking_path), permitted_classes: [], permitted_symbols: [], aliases: true)
  packs = data["installed_packs"] || {}

  packs.map do |name, meta|
    {
      "name" => name,
      "version" => meta["version"]
    }
  end
rescue
  []
end

.load_policies(dir) ⇒ Object

Loading



38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/vkit/policy/bundle_compiler.rb', line 38

def self.load_policies(dir)
  files = Dir[File.join(dir, "*.y{a,}ml")].sort
  raise "No policy files found in #{dir}" if files.empty?

  files.map do |f|
    data = YAML.load_file(f)
    raise "Policy file #{f} must be a Hash" unless data.is_a?(Hash)

    PolicyValidator.validate!(data, file: File.basename(f))

    data.merge("__file" => File.basename(f))
  end
end

.load_registry(dir) ⇒ Object



52
53
54
55
56
57
58
# File 'lib/vkit/policy/bundle_compiler.rb', line 52

def self.load_registry(dir)
  path = File.join(dir, "registry.yaml")
  raise "Missing datasets/registry.yaml" unless File.exist?(path)

  raw = YAML.load_file(path)
  normalize_registry(raw)
end

.normalize_action(action) ⇒ Object



91
92
93
94
95
96
97
98
# File 'lib/vkit/policy/bundle_compiler.rb', line 91

def self.normalize_action(action)
  return "allow" unless action.is_a?(Hash)

  return "deny" if action["deny"]
  return "require_approval" if action["require_approval"]
  return "mask" if action["mask"]
  "allow"
end

.normalize_fields(fields) ⇒ Object



155
156
157
158
159
160
161
162
163
164
# File 'lib/vkit/policy/bundle_compiler.rb', line 155

def self.normalize_fields(fields)
  fields.map do |name, meta|
    {
      "name" => name.to_s,
      "type" => meta["type"],
      "sensitivity" => meta["sensitivity"].to_s,
      "tags" => [meta["category"]].compact.map(&:to_s)
    }
  end
end

.normalize_mask_method(method) ⇒ Object



166
167
168
169
170
171
172
173
174
# File 'lib/vkit/policy/bundle_compiler.rb', line 166

def self.normalize_mask_method(method)
  case method.to_s
  when "redact"   then "full"
  when "hash"     then "hash"
  when "truncate" then "partial"
  when "nullify"  then "full"
  else "full"
  end
end

.normalize_policies(policies) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/vkit/policy/bundle_compiler.rb', line 65

def self.normalize_policies(policies)
  seen = {}

  normalized = policies.map do |p|
    id = p["id"].to_s.strip
    raise "Policy id missing in #{p["__file"]}" if id.empty?
    raise "Duplicate policy id: #{id}" if seen[id]
    seen[id] = true

    {
      "id" => id,
      "description" => p["description"],
      "match" => p["match"],
      "when" => p["context"],          # ADAPT authoring → runtime
      "action" => normalize_action(p["action"]),
      "reason" => p.dig("action", "reason"),
      "approval" => extract_approval(p),
      "masking" => extract_masking(p),
      "ttl_seconds" => p.dig("action", "ttl"),
      "priority" => p["priority"]
    }.compact
  end

  normalized.sort_by { |p| [-(p["priority"] || 0), p["id"]] }
end

.normalize_registry(raw) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/vkit/policy/bundle_compiler.rb', line 134

def self.normalize_registry(raw)
  datasets = raw.map do |name, data|
    {
      "name" => name.to_s,
      "datasource" => data["datasource"].to_s,
      "fields" => normalize_fields(data["fields"] || {})
    }
  end

  datasources =
    datasets
      .map { |d| d["datasource"] }
      .uniq
      .map { |ds| { "name" => ds, "type" => "postgres", "config" => {} } }

  {
    "datasets" => datasets,
    "datasources" => datasources
  }
end

.normalize_source(source) ⇒ Object

Normalization



61
62
63
# File 'lib/vkit/policy/bundle_compiler.rb', line 61

def self.normalize_source(source)
  { "type" => "git" }.merge(source.transform_keys(&:to_s))
end

.sort_keys_deep(value) ⇒ Object



181
182
183
184
185
186
187
188
189
190
# File 'lib/vkit/policy/bundle_compiler.rb', line 181

def self.sort_keys_deep(value)
  case value
  when Hash
    value.keys.sort.each_with_object({}) { |k, h| h[k] = sort_keys_deep(value[k]) }
  when Array
    value.map { |v| sort_keys_deep(v) }
  else
    value
  end
end