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

Class Attribute Details

.bpf_pin_dirObject



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


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


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 # AF_INET
    ipv4 = addr[0, 4].bytes.join(".")
    "#{ipv4}:#{port} (#{socket_const_name("AF_", family)})"
  when 10 # AF_INET6
    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

Returns:

  • (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