Module: Vivarium
- Defined in:
- lib/vivarium.rb,
lib/vivarium/logger.rb,
lib/vivarium/version.rb
Defined Under Namespace
Classes: Daemon, Error, Event, Logger, MapStore, ObservationSession
Constant Summary
collapse
- PIN_DIR =
ENV.fetch("VIVARIUM_BPF_PIN_DIR", "/sys/fs/bpf/vivarium")
- CONFIG_ROOT_TARGETS_PIN =
File.join(PIN_DIR, "config_root_targets")
- CONFIG_SPAWNED_TARGETS_PIN =
File.join(PIN_DIR, "config_spawned_targets")
- CONFIG_TARGETS_PIN =
CONFIG_ROOT_TARGETS_PIN
- EVENT_INVOKED_PIN =
File.join(PIN_DIR, "event_invoked")
- EVENT_WRITE_POS_PIN =
File.join(PIN_DIR, "event_write_pos")
- EVENT_NAME_SIZE =
16
- EVENT_PAYLOAD_SIZE =
256
- EVENT_TS_SIZE =
8
- PROC_EXEC_SLOT_SIZE =
64
- PROC_EXEC_SLOT_COUNT =
4
- EVENT_STRUCT_SIZE =
288
- EVENT_TS_OFFSET =
0
- EVENT_PID_OFFSET =
8
- EVENT_NAME_OFFSET =
12
- EVENT_PAYLOAD_OFFSET =
28
- EVENT_CAPACITY =
1024
- EVENT_SEVERITY_HIGH =
%w[
capable_check bprm_creds setid_change task_kill
ptrace_check sb_mount kernel_read_file
].freeze
- CAPABILITY_NAMES =
{
0 => "CAP_CHOWN",
1 => "CAP_DAC_OVERRIDE",
2 => "CAP_DAC_READ_SEARCH",
3 => "CAP_FOWNER",
4 => "CAP_FSETID",
5 => "CAP_KILL",
6 => "CAP_SETGID",
7 => "CAP_SETUID",
8 => "CAP_SETPCAP",
9 => "CAP_LINUX_IMMUTABLE",
10 => "CAP_NET_BIND_SERVICE",
12 => "CAP_NET_ADMIN",
13 => "CAP_NET_RAW",
16 => "CAP_SYS_MODULE",
17 => "CAP_SYS_RAWIO",
18 => "CAP_SYS_CHROOT",
19 => "CAP_SYS_PTRACE",
21 => "CAP_SYS_ADMIN",
22 => "CAP_SYS_BOOT",
23 => "CAP_SYS_NICE",
24 => "CAP_SYS_RESOURCE",
25 => "CAP_SYS_TIME",
27 => "CAP_MKNOD",
29 => "CAP_AUDIT_WRITE",
37 => "CAP_AUDIT_READ",
38 => "CAP_PERFMON",
39 => "CAP_BPF",
40 => "CAP_CHECKPOINT_RESTORE"
}.freeze
- SETID_FLAG_NAMES =
{
0x01 => "LSM_SETID_ID",
0x02 => "LSM_SETID_RE",
0x04 => "LSM_SETID_RES",
0x08 => "LSM_SETID_FS"
}.freeze
- VERSION =
"0.2.0"
Class Attribute Summary collapse
Class Method Summary
collapse
-
.build_observe_tracepoint(store, logger) ⇒ Object
-
.c_string(bytes) ⇒ Object
-
.decode_bad_socket_payload(raw_payload) ⇒ Object
-
.decode_bprm_creds_payload(raw_payload) ⇒ Object
-
.decode_capable_check_payload(raw_payload) ⇒ Object
-
.decode_dns_qname(raw_payload) ⇒ Object
-
.decode_file_chmod_payload(raw_payload) ⇒ Object
-
.decode_file_getdents_payload(raw_payload) ⇒ Object
-
.decode_file_hardlink_payload(raw_payload) ⇒ Object
-
.decode_file_rename_payload(raw_payload) ⇒ Object
-
.decode_file_symlink_payload(raw_payload) ⇒ Object
-
.decode_kernel_read_file_payload(raw_payload) ⇒ Object
-
.decode_odd_socket_payload(raw_payload) ⇒ Object
-
.decode_proc_exec_payload(raw_payload) ⇒ Object
-
.decode_ptrace_check_payload(raw_payload) ⇒ Object
-
.decode_sb_mount_payload(raw_payload) ⇒ Object
-
.decode_setid_change_payload(raw_payload) ⇒ Object
-
.decode_sock_connect_payload(raw_payload) ⇒ Object
-
.decode_task_kill_payload(raw_payload) ⇒ Object
-
.event_severity(event_name) ⇒ Object
-
.filter_internal_frames? ⇒ Boolean
-
.observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human) ⇒ Object
-
.render_event_payload(event) ⇒ Object
-
.run_daemon!(argv = ARGV) ⇒ Object
-
.scoped_observe(pin_dir:, logger:, dest:, format:) ⇒ Object
-
.socket_const_name(prefix, value) ⇒ Object
-
.top_observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human) ⇒ Object
Class Attribute Details
.bpf_pin_dir ⇒ Object
81
82
83
|
# File 'lib/vivarium.rb', line 81
def bpf_pin_dir
@bpf_pin_dir || PIN_DIR
end
|
Class Method Details
.build_observe_tracepoint(store, logger) ⇒ Object
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
|
# File 'lib/vivarium.rb', line 1521
def self.build_observe_tracepoint(store, logger)
TracePoint.new(:return, :c_return) do |tp|
events = store.drain_events
next if events.empty?
stack = caller_locations(2, 16)
stack = stack.reject { |loc| loc.path.to_s.include?("vivarium") } if filter_internal_frames?
logger.log(events, tp, stack)
end
end
|
.c_string(bytes) ⇒ Object
127
128
129
|
# File 'lib/vivarium.rb', line 127
def self.c_string(bytes)
Event.c_string(bytes)
end
|
.decode_bad_socket_payload(raw_payload) ⇒ Object
204
205
206
|
# File 'lib/vivarium.rb', line 204
def self.decode_bad_socket_payload(raw_payload)
decode_odd_socket_payload(raw_payload)
end
|
.decode_bprm_creds_payload(raw_payload) ⇒ Object
324
325
326
327
328
329
330
331
|
# File 'lib/vivarium.rb', line 324
def self.decode_bprm_creds_payload(raw_payload)
bytes = raw_payload.to_s.b
return "" if bytes.bytesize < 2
has_file = bytes.getbyte(0).to_i
path = c_string(bytes[1, EVENT_PAYLOAD_SIZE - 1])
"has_file=#{has_file} file=#{path.inspect}"
end
|
.decode_capable_check_payload(raw_payload) ⇒ Object
314
315
316
317
318
319
320
321
322
|
# File 'lib/vivarium.rb', line 314
def self.decode_capable_check_payload(raw_payload)
bytes = raw_payload.to_s.b
return "" if bytes.bytesize < 8
cap = bytes[0, 4].unpack1("L<")
opts = bytes[4, 4].unpack1("L<")
cap_name = CAPABILITY_NAMES.fetch(cap, "UNKNOWN")
"cap=#{cap}(#{cap_name}) opts=0x#{opts.to_s(16)}"
end
|
.decode_dns_qname(raw_payload) ⇒ Object
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
# File 'lib/vivarium.rb', line 135
def self.decode_dns_qname(raw_payload)
bytes = raw_payload.to_s.b.bytes
labels = []
idx = 0
while idx < bytes.length
length = bytes[idx]
break if length.nil? || length.zero?
break if length > 63
idx += 1
break if (idx + length) > bytes.length
label = bytes[idx, length].pack("C*")
labels << label
idx += length
end
return "" if labels.empty?
labels.join(".")
end
|
.decode_file_chmod_payload(raw_payload) ⇒ Object
229
230
231
232
233
234
235
236
|
# File 'lib/vivarium.rb', line 229
def self.decode_file_chmod_payload(raw_payload)
bytes = raw_payload.to_s.b
return "" if bytes.bytesize < 2
mode = bytes[0, 2].unpack1("S<")
path = c_string(bytes[2, 254])
"mode=#{format('0o%o', mode)} path=#{path.inspect}"
end
|
.decode_file_getdents_payload(raw_payload) ⇒ Object
238
239
240
241
242
243
244
245
|
# File 'lib/vivarium.rb', line 238
def self.decode_file_getdents_payload(raw_payload)
bytes = raw_payload.to_s.b
return "" if bytes.bytesize < 8
fd = bytes[0, 4].unpack1("L<")
count = bytes[4, 4].unpack1("L<")
"fd=#{fd} count=#{count}"
end
|
.decode_file_hardlink_payload(raw_payload) ⇒ Object
215
216
217
218
219
220
|
# File 'lib/vivarium.rb', line 215
def self.decode_file_hardlink_payload(raw_payload)
bytes = raw_payload.to_s.b
old_path = c_string(bytes[0, 128])
new_name = c_string(bytes[128, 128])
"old_path=#{old_path.inspect} new_name=#{new_name.inspect}"
end
|
.decode_file_rename_payload(raw_payload) ⇒ Object
222
223
224
225
226
227
|
# File 'lib/vivarium.rb', line 222
def self.decode_file_rename_payload(raw_payload)
bytes = raw_payload.to_s.b
old_name = c_string(bytes[0, 128])
new_name = c_string(bytes[128, 128])
"old_name=#{old_name.inspect} new_name=#{new_name.inspect}"
end
|
.decode_file_symlink_payload(raw_payload) ⇒ Object
208
209
210
211
212
213
|
# File 'lib/vivarium.rb', line 208
def self.decode_file_symlink_payload(raw_payload)
bytes = raw_payload.to_s.b
target = c_string(bytes[0, 128])
link_name = c_string(bytes[128, 128])
"target=#{target.inspect} link_name=#{link_name.inspect}"
end
|
.decode_kernel_read_file_payload(raw_payload) ⇒ Object
279
280
281
282
283
284
285
286
|
# File 'lib/vivarium.rb', line 279
def self.decode_kernel_read_file_payload(raw_payload)
bytes = raw_payload.to_s.b
return "" if bytes.bytesize < 8
id = bytes[0, 4].unpack1("L<")
contents = bytes[4, 4].unpack1("L<")
"id=#{id} contents=#{contents}"
end
|
.decode_odd_socket_payload(raw_payload) ⇒ Object
179
180
181
182
183
184
185
186
187
188
189
190
|
# File 'lib/vivarium.rb', line 179
def self.decode_odd_socket_payload(raw_payload)
bytes = raw_payload.to_s.b
return "" if bytes.bytesize < 6
family = bytes[0, 2].unpack1("S<")
type = bytes[2, 2].unpack1("S<")
protocol = bytes[4, 2].unpack1("S<")
family_name = socket_const_name("AF_", family)
type_name = socket_const_name("SOCK_", type)
protocol_name = socket_const_name("IPPROTO_", protocol)
"family=#{family}(#{family_name}) type=#{type}(#{type_name}) protocol=#{protocol}(#{protocol_name})"
end
|
.decode_proc_exec_payload(raw_payload) ⇒ Object
247
248
249
250
251
252
253
254
255
256
257
258
259
|
# File 'lib/vivarium.rb', line 247
def self.decode_proc_exec_payload(raw_payload)
bytes = raw_payload.to_s.b
slots = PROC_EXEC_SLOT_COUNT.times.map do |index|
offset = index * PROC_EXEC_SLOT_SIZE
c_string(bytes[offset, PROC_EXEC_SLOT_SIZE])
end
slots.reject!(&:empty?)
return "" if slots.empty?
filename = slots.shift
argv = slots
"filename=#{filename.inspect} argv=[#{argv.map(&:inspect).join(', ')}]"
end
|
.decode_ptrace_check_payload(raw_payload) ⇒ Object
261
262
263
264
265
266
267
|
# File 'lib/vivarium.rb', line 261
def self.decode_ptrace_check_payload(raw_payload)
bytes = raw_payload.to_s.b
return "" if bytes.bytesize < 4
mode = bytes[0, 4].unpack1("L<")
"mode=0x#{mode.to_s(16)}"
end
|
.decode_sb_mount_payload(raw_payload) ⇒ Object
269
270
271
272
273
274
275
276
277
|
# File 'lib/vivarium.rb', line 269
def self.decode_sb_mount_payload(raw_payload)
bytes = raw_payload.to_s.b
return "" if bytes.bytesize < 248
flags = bytes[0, 8].unpack1("Q<")
dev_name = c_string(bytes[8, 120])
fs_type = c_string(bytes[128, 120])
"flags=0x#{flags.to_s(16)} dev_name=#{dev_name.inspect} fs_type=#{fs_type.inspect}"
end
|
.decode_setid_change_payload(raw_payload) ⇒ Object
302
303
304
305
306
307
308
309
310
311
312
|
# File 'lib/vivarium.rb', line 302
def self.decode_setid_change_payload(raw_payload)
bytes = raw_payload.to_s.b
return "" if bytes.bytesize < 4
flags = bytes[0, 4].unpack1("L<")
names = SETID_FLAG_NAMES.each_with_object([]) do |(bit, name), acc|
acc << name if (flags & bit) != 0
end
names << "UNKNOWN" if names.empty?
"flags=0x#{flags.to_s(16)} kinds=[#{names.join(', ')}]"
end
|
.decode_sock_connect_payload(raw_payload) ⇒ Object
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
|
# File 'lib/vivarium.rb', line 158
def self.decode_sock_connect_payload(raw_payload)
bytes = raw_payload.to_s.b
return "" if bytes.bytesize < 20
family = bytes[0, 2].unpack1("S<")
port = bytes[2, 2].unpack1("n")
addr = bytes[4, 16]
case family
when 2 ipv4 = addr[0, 4].bytes.join(".")
"#{ipv4}:#{port} (#{socket_const_name("AF_", family)})"
when 10 words = addr.unpack("n8")
ipv6 = words.map { |w| format("%x", w) }.join(":")
"[#{ipv6}]:#{port} (#{socket_const_name("AF_", family)})"
else
"family=#{family}(#{socket_const_name("AF_", family)}) port=#{port}"
end
end
|
.decode_task_kill_payload(raw_payload) ⇒ Object
288
289
290
291
292
293
294
295
296
297
298
299
300
|
# File 'lib/vivarium.rb', line 288
def self.decode_task_kill_payload(raw_payload)
bytes = raw_payload.to_s.b
return "" if bytes.bytesize < 4
sig = bytes[0, 4].unpack1("l<")
signame = begin
Signal.signame(sig)
rescue ArgumentError
nil
end
signame ? "sig=#{sig} signame=#{signame}" : "sig=#{sig}"
end
|
.event_severity(event_name) ⇒ Object
131
132
133
|
# File 'lib/vivarium.rb', line 131
def self.event_severity(event_name)
EVENT_SEVERITY_HIGH.include?(event_name.to_s) ? "high" : "medium"
end
|
.filter_internal_frames? ⇒ Boolean
1532
1533
1534
1535
1536
1537
|
# File 'lib/vivarium.rb', line 1532
def self.filter_internal_frames?
value = ENV["VIVARIUM_FILTER_INTERNAL_FRAMES"]
return true if value.nil?
!%w[0 false off no].include?(value.strip.downcase)
end
|
.observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human) ⇒ Object
1484
1485
1486
1487
1488
|
# File 'lib/vivarium.rb', line 1484
def self.observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human)
return scoped_observe(pin_dir: pin_dir, logger: logger, dest: dest, format: format) { yield } if block_given?
top_observe(pin_dir: pin_dir, logger: logger, dest: dest, format: format)
end
|
.render_event_payload(event) ⇒ Object
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
|
# File 'lib/vivarium.rb', line 333
def self.render_event_payload(event)
case event.event_name
when "dns_req"
decoded = decode_dns_qname(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "sock_connect"
decoded = decode_sock_connect_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "odd_socket"
decoded = decode_odd_socket_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "proc_exec"
decoded = decode_proc_exec_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "ptrace_check"
decoded = decode_ptrace_check_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "sb_mount"
decoded = decode_sb_mount_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "kernel_read_file"
decoded = decode_kernel_read_file_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "task_kill"
decoded = decode_task_kill_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "setid_change"
decoded = decode_setid_change_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "capable_check"
decoded = decode_capable_check_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "bprm_creds"
decoded = decode_bprm_creds_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "file_symlink"
decoded = decode_file_symlink_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "file_hardlink"
decoded = decode_file_hardlink_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "file_rename"
decoded = decode_file_rename_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "file_chmod"
decoded = decode_file_chmod_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
when "file_getdents"
decoded = decode_file_getdents_payload(event.payload)
decoded.empty? ? event.payload.inspect : decoded
else
event.payload.inspect
end
end
|
.run_daemon!(argv = ARGV) ⇒ Object
1539
1540
1541
1542
1543
1544
1545
1546
1547
|
# File 'lib/vivarium.rb', line 1539
def self.run_daemon!(argv = ARGV)
options = { pin_dir: bpf_pin_dir }
OptionParser.new do |opts|
opts.banner = "Usage: vivariumd [--pin-dir PATH]"
opts.on("--pin-dir PATH", "Pinned map directory") { |v| options[:pin_dir] = v }
end.parse!(argv)
Daemon.new(pin_dir: options[:pin_dir]).run
end
|
.scoped_observe(pin_dir:, logger:, dest:, format:) ⇒ Object
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
|
# File 'lib/vivarium.rb', line 1505
def self.scoped_observe(pin_dir:, logger:, dest:, format:)
logger ||= Logger.new(dest: dest, format: format)
store = MapStore.new(pin_dir: pin_dir)
pid = Process.pid
store.register_pid(pid)
logger.info("scoped observing with pid=#{pid}")
tracer = build_observe_tracepoint(store, logger)
tracer.enable
yield
ensure
tracer&.disable
store&.unregister_pid(pid)
end
|
.socket_const_name(prefix, value) ⇒ Object
192
193
194
195
196
197
198
199
200
201
202
|
# File 'lib/vivarium.rb', line 192
def self.socket_const_name(prefix, value)
return "UNKNOWN" unless defined?(Socket)
key = Socket.constants.find do |name|
name.to_s.start_with?(prefix) && Socket.const_get(name) == value
rescue NameError
false
end
key ? key.to_s : "UNKNOWN"
end
|
.top_observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human) ⇒ Object
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
|
# File 'lib/vivarium.rb', line 1490
def self.top_observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human)
logger ||= Logger.new(dest: dest, format: format)
store = MapStore.new(pin_dir: pin_dir)
pid = Process.pid
store.register_pid(pid)
logger.info("top-level observing with pid=#{pid}")
tracer = build_observe_tracepoint(store, logger)
tracer.enable
session = ObservationSession.new(store: store, pid: pid, tracer: tracer)
at_exit { session.stop }
session
end
|