vaultak 0.6.1
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 +7 -0
- data/lib/vaultak.rb +226 -0
- metadata +45 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a3cd3eb63bb56e4ceb3ff51ec94fdc8f338b4813f2a962155f64fae6bf971cb4
|
|
4
|
+
data.tar.gz: 2a3958bc73a515d5f5ced9bd0bc43e5f67d84b62cdf4ec7bcb4d2453ffdedcfe
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 26366a685be751a9a4cbda61e98b7d7d614053a4a214f7f95292f6d35e948370bacc1d97f7084feb935ec133bcb653cda5ce22736b9c2659f9ad96963ec4c9e8
|
|
7
|
+
data.tar.gz: 2e3ee2cf02a2b24c13b70be9d9060865575476a1ecb7a0a39e72182ee65e8868d794c2480e8ee8083580df422ee0ce36d936d40b7f95b357ec598851cb284fd5
|
data/lib/vaultak.rb
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'digest'
|
|
6
|
+
require 'time'
|
|
7
|
+
|
|
8
|
+
module Vaultak
|
|
9
|
+
VERSION = "0.6.1"
|
|
10
|
+
|
|
11
|
+
class Client
|
|
12
|
+
def initialize(options = {})
|
|
13
|
+
@api_key = options[:api_key] || ENV['VAULTAK_API_KEY'] || ''
|
|
14
|
+
@agent_id = options[:agent_id] || 'default'
|
|
15
|
+
@alert_threshold = options[:alert_threshold] || 30
|
|
16
|
+
@pause_threshold = options[:pause_threshold] || 60
|
|
17
|
+
@rollback_threshold = options[:rollback_threshold] || 85
|
|
18
|
+
@blocked_resources = options[:blocked_resources] || []
|
|
19
|
+
@allowed_resources = options[:allowed_resources]
|
|
20
|
+
@max_actions_per_minute = options[:max_actions_per_minute] || 60
|
|
21
|
+
@api_endpoint = options[:api_endpoint] || 'https://vaultak.com'
|
|
22
|
+
@session_id = SecureRandom.uuid rescue "#{Time.now.to_i}-#{rand(9999)}"
|
|
23
|
+
@action_times = []
|
|
24
|
+
@file_snapshots = {}
|
|
25
|
+
@paused = false
|
|
26
|
+
@originals = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def monitor(agent_id = nil, &block)
|
|
30
|
+
@current_agent_id = agent_id || @agent_id
|
|
31
|
+
@_installed = false
|
|
32
|
+
_install
|
|
33
|
+
@_installed = true
|
|
34
|
+
begin
|
|
35
|
+
yield self if block_given?
|
|
36
|
+
rescue VaultakPauseError, VaultakBlockError
|
|
37
|
+
_uninstall if @_installed
|
|
38
|
+
@_installed = false
|
|
39
|
+
raise
|
|
40
|
+
ensure
|
|
41
|
+
_uninstall if @_installed
|
|
42
|
+
@_installed = false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def intercept(action_type, resource, payload = {})
|
|
47
|
+
return 'BLOCK' if @paused
|
|
48
|
+
|
|
49
|
+
# Rate limiting
|
|
50
|
+
now = Time.now.to_f
|
|
51
|
+
@action_times.reject! { |t| now - t > 60 }
|
|
52
|
+
if @action_times.length >= @max_actions_per_minute
|
|
53
|
+
_send_action(action_type, resource, payload, 90, 'BLOCK')
|
|
54
|
+
return 'BLOCK'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Policy checks
|
|
58
|
+
@blocked_resources.each do |pattern|
|
|
59
|
+
if File.fnmatch(pattern, resource) || resource.include?(pattern.gsub('*', ''))
|
|
60
|
+
_send_action(action_type, resource, payload, 95, 'BLOCK')
|
|
61
|
+
return 'BLOCK'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if @allowed_resources
|
|
66
|
+
allowed = @allowed_resources.any? { |p| File.fnmatch(p, resource) }
|
|
67
|
+
unless allowed
|
|
68
|
+
_send_action(action_type, resource, payload, 80, 'BLOCK')
|
|
69
|
+
return 'BLOCK'
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
score = _compute_score(action_type, resource)
|
|
74
|
+
|
|
75
|
+
if score >= @rollback_threshold
|
|
76
|
+
_send_action(action_type, resource, payload, score, 'ROLLBACK')
|
|
77
|
+
_execute_rollback
|
|
78
|
+
@paused = true
|
|
79
|
+
raise VaultakPauseError, "Risk score #{score} exceeded rollback threshold. State restored."
|
|
80
|
+
elsif score >= @pause_threshold
|
|
81
|
+
_send_action(action_type, resource, payload, score, 'PAUSE')
|
|
82
|
+
@paused = true
|
|
83
|
+
raise VaultakPauseError, "Risk score #{score} exceeded pause threshold. Awaiting review."
|
|
84
|
+
elsif score >= @alert_threshold
|
|
85
|
+
_send_action(action_type, resource, payload, score, 'ALERT')
|
|
86
|
+
else
|
|
87
|
+
_send_action(action_type, resource, payload, score, 'ALLOW')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@action_times << now
|
|
91
|
+
'ALLOW'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def snapshot_file(path)
|
|
95
|
+
if File.exist?(path)
|
|
96
|
+
@file_snapshots[path] = File.binread(path)
|
|
97
|
+
else
|
|
98
|
+
@file_snapshots[path] = nil
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def approve
|
|
103
|
+
@paused = false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def log_action(action_type, resource, payload = {})
|
|
107
|
+
_send_action(action_type, resource, payload, 0, 'ALLOW')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def _install
|
|
113
|
+
client = self
|
|
114
|
+
|
|
115
|
+
# Patch File.write
|
|
116
|
+
@originals[:file_write] = File.method(:write)
|
|
117
|
+
File.define_singleton_method(:write) do |path, *args|
|
|
118
|
+
client.snapshot_file(path.to_s)
|
|
119
|
+
decision = client.intercept('file_write', path.to_s, { method: 'File.write' })
|
|
120
|
+
raise VaultakBlockError, "File write blocked: #{path}" if decision == 'BLOCK'
|
|
121
|
+
client.instance_variable_get(:@originals)[:file_write].call(path, *args)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Patch File.delete
|
|
125
|
+
@originals[:file_delete] = File.method(:delete)
|
|
126
|
+
File.define_singleton_method(:delete) do |*paths|
|
|
127
|
+
paths.each do |path|
|
|
128
|
+
decision = client.intercept('delete', path.to_s, {})
|
|
129
|
+
raise VaultakBlockError, "File delete blocked: #{path}" if decision == 'BLOCK'
|
|
130
|
+
end
|
|
131
|
+
client.instance_variable_get(:@originals)[:file_delete].call(*paths)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
$stderr.puts "[Vaultak] Ruby agent monitoring active: #{@current_agent_id}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def _uninstall
|
|
138
|
+
return if @originals.empty?
|
|
139
|
+
# Store originals locally before clearing
|
|
140
|
+
orig_write = @originals[:file_write]
|
|
141
|
+
orig_delete = @originals[:file_delete]
|
|
142
|
+
@originals = {}
|
|
143
|
+
# Restore File methods using stored originals
|
|
144
|
+
if orig_write
|
|
145
|
+
File.define_singleton_method(:write) { |*args| orig_write.call(*args) }
|
|
146
|
+
end
|
|
147
|
+
if orig_delete
|
|
148
|
+
File.define_singleton_method(:delete) { |*args| orig_delete.call(*args) }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def _compute_score(action_type, resource)
|
|
153
|
+
scores = {
|
|
154
|
+
'file_write' => 40, 'file_read' => 10, 'delete' => 75,
|
|
155
|
+
'api_call' => 35, 'execute' => 60, 'database_write' => 50,
|
|
156
|
+
'database_read' => 15
|
|
157
|
+
}
|
|
158
|
+
score = scores[action_type] || 30
|
|
159
|
+
sensitive = %w[prod production secret .env password key token credential]
|
|
160
|
+
score += 30 if sensitive.any? { |p| resource.downcase.include?(p) }
|
|
161
|
+
[score, 100].min
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def _execute_rollback
|
|
165
|
+
orig_delete = @originals[:file_delete]
|
|
166
|
+
@file_snapshots.each do |path, snapshot|
|
|
167
|
+
begin
|
|
168
|
+
if snapshot.nil?
|
|
169
|
+
if File.exist?(path)
|
|
170
|
+
if orig_delete
|
|
171
|
+
orig_delete.call(path)
|
|
172
|
+
else
|
|
173
|
+
File.unlink(path)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
else
|
|
177
|
+
File.binwrite(path, snapshot)
|
|
178
|
+
end
|
|
179
|
+
$stderr.puts "[Vaultak] Rolled back: #{path}"
|
|
180
|
+
rescue => e
|
|
181
|
+
$stderr.puts "[Vaultak] Rollback failed for #{path}: #{e.message}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
@file_snapshots = {}
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def _send_action(action_type, resource, payload, score, decision)
|
|
188
|
+
data = {
|
|
189
|
+
agent_id: @current_agent_id || @agent_id,
|
|
190
|
+
session_id: @session_id,
|
|
191
|
+
action_type: action_type,
|
|
192
|
+
resource: resource,
|
|
193
|
+
payload: payload,
|
|
194
|
+
risk_score: score / 100.0,
|
|
195
|
+
decision: decision,
|
|
196
|
+
timestamp: Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
197
|
+
source: 'ruby-sdk'
|
|
198
|
+
}.to_json
|
|
199
|
+
|
|
200
|
+
Thread.new do
|
|
201
|
+
begin
|
|
202
|
+
uri = URI("#{@api_endpoint}/api/actions")
|
|
203
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
204
|
+
http.use_ssl = uri.scheme == 'https'
|
|
205
|
+
http.open_timeout = 3
|
|
206
|
+
http.read_timeout = 3
|
|
207
|
+
req = Net::HTTP::Post.new(uri.path)
|
|
208
|
+
req['Content-Type'] = 'application/json'
|
|
209
|
+
req['x-api-key'] = @api_key
|
|
210
|
+
req.body = data
|
|
211
|
+
http.request(req)
|
|
212
|
+
rescue
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
class VaultakPauseError < StandardError; end
|
|
219
|
+
class VaultakBlockError < StandardError; end
|
|
220
|
+
|
|
221
|
+
# Convenience method
|
|
222
|
+
def self.monitor(agent_id, options = {}, &block)
|
|
223
|
+
client = Client.new(options)
|
|
224
|
+
client.monitor(agent_id, &block)
|
|
225
|
+
end
|
|
226
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: vaultak
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.6.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Vaultak
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Monitor every action, enforce policies, and automatically roll back damage
|
|
14
|
+
from AI agents.
|
|
15
|
+
email:
|
|
16
|
+
- support@vaultak.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- lib/vaultak.rb
|
|
22
|
+
homepage: https://vaultak.com
|
|
23
|
+
licenses:
|
|
24
|
+
- MIT
|
|
25
|
+
metadata: {}
|
|
26
|
+
post_install_message:
|
|
27
|
+
rdoc_options: []
|
|
28
|
+
require_paths:
|
|
29
|
+
- lib
|
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - ">="
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: 2.7.0
|
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
requirements: []
|
|
41
|
+
rubygems_version: 3.0.3.1
|
|
42
|
+
signing_key:
|
|
43
|
+
specification_version: 4
|
|
44
|
+
summary: Runtime security for autonomous AI agents
|
|
45
|
+
test_files: []
|