vivarium 0.1.2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fb7901d1f200e910b88b75fd8ba54cbaa2d6cf2152c31e35b6f790e8b9150e5
4
- data.tar.gz: 045c6ef47acb148605a7dd7cbbda5526d82d3548191135baedecaa5d09840f25
3
+ metadata.gz: 973df86f00b8e2ca9d5e963958562e2f58822e13c0e4d33af53e85a8d38f12be
4
+ data.tar.gz: a5651655ef69c10e6b6eab89baf9f98e5d99a158a8e89a6fc940fe2d844ab636
5
5
  SHA512:
6
- metadata.gz: 9042d44b860bde781d512e36367bfc178a74dd57c6ae6be49c5cea73ec4fbd8b621382ace5d0e1b18188e36bae41d3f5e7e273fd8e019c1ec88820f95653b255
7
- data.tar.gz: 9c22285362cea0fe871af6c1f67a66eeb85556a52c5f6aabe93da2b20daa51cbf6528df9021ead0fbb2acdcb152bbee60a1a873c9d7a9b75b1edc8ea244c6016
6
+ metadata.gz: 3606b1f6df38a813e2b0cdf48aef48312be1a7c0bd3f20f88ca8a4c527c724733184c1c0d52f39fc9242282ab6d41ca18f6bcc977f4cafad0c5d4faa17a27d78
7
+ data.tar.gz: 4171eefdd7052d8634cd5a49267c8fe4f925e87f5b18691cb0721dd3f014efac68475196762e9ecc800627bcc529d9077e088b7d0366d91a6252867d3a0fc5f1
data/README.md CHANGED
@@ -18,9 +18,20 @@ The goal is to visualize which Ruby method context triggered low-level events.
18
18
  Implemented in this repository:
19
19
 
20
20
  - BPF LSM hook on `file_open`
21
+ - BPF LSM hooks on `inode_symlink`, `inode_link`, `inode_rename`, `path_chmod`
22
+ - BPF tracepoint on `sys_enter_getdents64`
23
+ - BPF tracepoint on `sys_enter_execve` (captures executable path and first few argv entries as `proc_exec`)
24
+ - BPF LSM hooks for suspicious behavior checks:
25
+ - `ptrace_access_check` (emits `ptrace_check`)
26
+ - `sb_mount` (emits `sb_mount`)
27
+ - `kernel_read_file` (emits `kernel_read_file`)
28
+ - `task_kill` (emits `task_kill`)
29
+ - `task_fix_setuid` (emits `setid_change`)
30
+ - `capable` for high-risk capabilities only (emits `capable_check`)
31
+ - `bprm_creds_from_file` (emits `bprm_creds`)
21
32
  - BPF LSM hook on `socket_create` (flags unusual socket creation as `odd_socket`)
22
33
  - BPF LSM hook on `socket_connect` (captures destination family/address/port as `sock_connect`)
23
- - BPF kprobe on `ip_local_out` (captures UDP/53 DNS QNAME raw bytes as `dns_req`)
34
+ - BPF tracepoints on `sys_enter_sendmsg`, `sys_enter_sendto`, `sys_enter_sendmmsg` (capture UDP/53 DNS QNAME raw bytes as `dns_req`)
24
35
  - Shared pinned maps on bpffs
25
36
  - `config_root_targets` (root PID -> 0/1)
26
37
  - `config_spawned_targets` (spawned TID -> 0/1)
@@ -38,6 +49,7 @@ Implemented in this repository:
38
49
 
39
50
  ```c
40
51
  struct event_t {
52
+ u64 ktime_ns;
41
53
  u32 pid;
42
54
  char event_name[16];
43
55
  char payload[256];
@@ -48,7 +60,7 @@ struct event_t {
48
60
 
49
61
  - Linux kernel/environment supporting BPF LSM
50
62
  - `libbcc` installed
51
- - `bpftool` installed (used to resolve `struct file::f_path` offset from BTF)
63
+ - `bpftool` installed (used to resolve `struct file::f_path` and `struct dentry::d_name` offsets from BTF)
52
64
  - root privileges for `vivariumd`
53
65
  - bpffs mounted (typically `/sys/fs/bpf`)
54
66
 
@@ -92,6 +104,38 @@ bundle exec ruby examples/network_client_demo.rb
92
104
 
93
105
  This demo intentionally triggers `sock_connect`, `dns_req`, and `odd_socket` events.
94
106
 
107
+ 4) File operation demo client (only touches `/tmp`):
108
+
109
+ ```bash
110
+ bundle exec ruby examples/file_operation_demo.rb
111
+ ```
112
+
113
+ This demo intentionally triggers `path_open`, `file_symlink`, `file_hardlink`, `file_rename`, `file_chmod`, and `file_getdents` events under `/tmp`.
114
+
115
+ 5) Execve demo client:
116
+
117
+ ```bash
118
+ bundle exec ruby examples/execve_demo.rb
119
+ ```
120
+
121
+ This demo intentionally triggers `proc_exec` with several argument patterns using direct `execve`-style process launches.
122
+
123
+ 6) Signal demo client:
124
+
125
+ ```bash
126
+ bundle exec ruby examples/signal_kill_demo.rb
127
+ ```
128
+
129
+ This demo forks a child process and sends `TERM` with `Process.kill`, which is useful for triggering `task_kill`.
130
+
131
+ 7) Privilege-related event demo client:
132
+
133
+ ```bash
134
+ bundle exec ruby examples/privilege_event_demo.rb
135
+ ```
136
+
137
+ This demo attempts setuid/setgid changes, sensitive file access, and `sudo` exec to trigger privilege-related events such as `setid_change`, `capable_check`, and `bprm_creds`.
138
+
95
139
  You can also start top-level observation without a block (it keeps observing until process exit):
96
140
 
97
141
  ```ruby
@@ -135,11 +179,16 @@ bundle exec vivariumd --pin-dir /sys/fs/bpf/vivarium
135
179
  ## Notes
136
180
 
137
181
  - Thread/Ractor-awareness is not yet implemented.
138
- - `event_invoked` uses fixed 64 slots and wraps around when full.
139
- - Payload is truncated to 64 bytes in kernel space.
182
+ - `event_invoked` uses fixed 1024 slots and wraps around when full.
183
+ - `payload` is 256 bytes in `event_t`; some event types intentionally use smaller structured slices inside that buffer.
184
+ - `proc_exec` currently stores the executable path plus up to 3 argv entries in 4 fixed 64-byte slots to keep the BPF verifier happy.
185
+ - Each event is tagged with severity metadata: `high` for `setid_change`, `capable_check`, `bprm_creds`, `task_kill`, `ptrace_check`, `sb_mount`, and `kernel_read_file`; others are `medium` by default.
186
+ - In `human` format output, `high` severity events are rendered in red.
187
+ - `capable_check` is intentionally filtered to high-risk capabilities to reduce noise from extremely frequent `capable` hook calls.
140
188
  - Current output format is textual and intended for iteration.
141
189
  - `vivariumd` resolves `struct file::f_path` offset from `/sys/kernel/btf/vmlinux` at startup.
142
- - You can override offset manually with `VIVARIUM_FILE_F_PATH_OFFSET` if auto-detection fails.
190
+ - `vivariumd` also resolves `struct dentry::d_name` offset from `/sys/kernel/btf/vmlinux` at startup.
191
+ - You can override offsets manually with `VIVARIUM_FILE_F_PATH_OFFSET` and `VIVARIUM_DENTRY_D_NAME_OFFSET` if auto-detection fails.
143
192
  - `vivariumd` also prints `bpf_trace_printk` lines (`vivarium: pid=... path=...`) to its own logs.
144
193
 
145
194
  ## Contributing
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "tmpdir"
5
+ require "vivarium"
6
+
7
+ # Usage:
8
+ # 1) In another shell (root): sudo bundle exec vivariumd
9
+ # 2) Run this script: bundle exec ruby examples/execve_demo.rb
10
+
11
+ TMP_PREFIX = "vivarium-exec-demo"
12
+
13
+ def try_step(title)
14
+ puts "[exec-demo] #{title}"
15
+ yield
16
+ rescue StandardError => e
17
+ puts "[exec-demo] #{title} failed: #{e.class}: #{e.message}"
18
+ end
19
+
20
+ Dir.mktmpdir(TMP_PREFIX, "/tmp") do |dir|
21
+ output_path = File.join(dir, "execve-demo.out")
22
+
23
+ Vivarium.observe do
24
+ try_step("system echo with multiple args") do
25
+ system("/bin/echo", "hello", "from", "vivarium", out: File::NULL)
26
+ end
27
+
28
+ try_step("spawn env with explicit argv") do
29
+ pid = Process.spawn(
30
+ "/usr/bin/env",
31
+ "env",
32
+ "printf",
33
+ "execve-demo\n",
34
+ out: output_path,
35
+ err: File::NULL
36
+ )
37
+ Process.wait(pid)
38
+ end
39
+
40
+ try_step("spawn sleep with flag") do
41
+ pid = Process.spawn("/bin/sleep", "0")
42
+ Process.wait(pid)
43
+ end
44
+ end
45
+
46
+ puts "[exec-demo] output file: #{output_path}" if File.exist?(output_path)
47
+ end
48
+
49
+ puts "[exec-demo] done"
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "tmpdir"
6
+ require "vivarium"
7
+
8
+ # Usage:
9
+ # 1) In another shell (root): sudo bundle exec vivariumd
10
+ # 2) Run this script: bundle exec ruby examples/file_operation_demo.rb
11
+
12
+ TMP_PREFIX = "vivarium-file-demo"
13
+
14
+ def try_step(title)
15
+ puts "[file-demo] #{title}"
16
+ yield
17
+ rescue StandardError => e
18
+ puts "[file-demo] #{title} failed: #{e.class}: #{e.message}"
19
+ end
20
+
21
+ Dir.mktmpdir(TMP_PREFIX, "/tmp") do |dir|
22
+ source_path = File.join(dir, "source.txt")
23
+ renamed_path = File.join(dir, "renamed.txt")
24
+ hardlink_path = File.join(dir, "hardlink.txt")
25
+ symlink_path = File.join(dir, "symlink.txt")
26
+
27
+ Vivarium.observe do
28
+ try_step("create source file") do
29
+ File.write(source_path, "vivarium sample\n")
30
+ File.read(source_path)
31
+ end
32
+
33
+ try_step("directory listing") do
34
+ Dir.children(dir)
35
+ end
36
+
37
+ try_step("rename file") do
38
+ File.rename(source_path, renamed_path)
39
+ File.read(renamed_path)
40
+ end
41
+
42
+ try_step("create hardlink") do
43
+ File.link(renamed_path, hardlink_path)
44
+ File.read(hardlink_path)
45
+ end
46
+
47
+ try_step("create symlink") do
48
+ File.symlink(renamed_path, symlink_path)
49
+ File.read(symlink_path)
50
+ end
51
+
52
+ try_step("chmod file") do
53
+ File.chmod(0o640, renamed_path)
54
+ File.stat(renamed_path)
55
+ end
56
+
57
+ try_step("list directory again") do
58
+ Dir.children(dir)
59
+ end
60
+ end
61
+
62
+ FileUtils.rm_f(symlink_path)
63
+ FileUtils.rm_f(hardlink_path)
64
+ FileUtils.rm_f(renamed_path)
65
+ FileUtils.rm_f(source_path)
66
+ end
67
+
68
+ puts "[file-demo] done"
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "vivarium"
5
+
6
+ # Usage:
7
+ # 1) In another shell (root): sudo bundle exec vivariumd
8
+ # 2) Run this script: bundle exec ruby examples/privilege_event_demo.rb
9
+
10
+ def try_step(title)
11
+ puts "[priv-demo] #{title}"
12
+ yield
13
+ rescue StandardError => e
14
+ puts "[priv-demo] #{title} failed: #{e.class}: #{e.message}"
15
+ end
16
+
17
+ Vivarium.observe do
18
+ try_step("attempt setuid(0)") do
19
+ Process::UID.change_privilege(0)
20
+ end
21
+
22
+ try_step("attempt setgid(0)") do
23
+ Process::GID.change_privilege(0)
24
+ end
25
+
26
+ try_step("attempt opening /etc/shadow") do
27
+ File.read("/etc/shadow")
28
+ end
29
+
30
+ try_step("exec setuid-related binary") do
31
+ pid = Process.spawn("/usr/bin/sudo", "-n", "true", out: File::NULL, err: File::NULL)
32
+ Process.wait(pid)
33
+ rescue Errno::ENOENT
34
+ puts "[priv-demo] sudo not found; skipped"
35
+ end
36
+ end
37
+
38
+ puts "[priv-demo] done"
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "vivarium"
5
+
6
+ # Usage:
7
+ # 1) In another shell (root): sudo bundle exec vivariumd
8
+ # 2) Run this script: bundle exec ruby examples/signal_kill_demo.rb
9
+
10
+ def try_step(title)
11
+ puts "[signal-demo] #{title}"
12
+ yield
13
+ rescue StandardError => e
14
+ puts "[signal-demo] #{title} failed: #{e.class}: #{e.message}"
15
+ end
16
+
17
+ child_pid = nil
18
+
19
+ Vivarium.observe do
20
+ try_step("fork child process") do
21
+ child_pid = fork do
22
+ trap("TERM") { exit!(0) }
23
+ loop { sleep 1 }
24
+ end
25
+ puts "[signal-demo] child pid=#{child_pid}"
26
+ end
27
+
28
+ try_step("send TERM signal to child") do
29
+ sleep 0.1
30
+ Process.kill("TERM", child_pid)
31
+ end
32
+
33
+ try_step("wait child process") do
34
+ Process.wait(child_pid)
35
+ end
36
+ end
37
+
38
+ puts "[signal-demo] done"
@@ -5,6 +5,8 @@ require "json"
5
5
  module Vivarium
6
6
  class Logger
7
7
  FORMATS = %i[human json].freeze
8
+ ANSI_RED = "\e[31m"
9
+ ANSI_RESET = "\e[0m"
8
10
 
9
11
  # dest: IO object or file path string
10
12
  # format: :human or :json
@@ -45,7 +47,9 @@ module Vivarium
45
47
  @io.puts "[vivarium] #{events.size} event(s) at #{tp.defined_class}##{tp.method_id} (#{tp.event})"
46
48
  @io.puts " location: #{tp.path}:#{tp.lineno}"
47
49
  events.each do |event|
48
- @io.puts " ktime_ns=#{event.ktime_ns} pid=#{event.pid} #{event.event_name} payload=#{Vivarium.render_event_payload(event)}"
50
+ severity = event.respond_to?(:severity) ? event.severity : Vivarium.event_severity(event.event_name)
51
+ line = " ktime_ns=#{event.ktime_ns} pid=#{event.pid} severity=#{severity} #{event.event_name} payload=#{Vivarium.render_event_payload(event)}"
52
+ @io.puts(severity == "high" ? "#{ANSI_RED}#{line}#{ANSI_RESET}" : line)
49
53
  end
50
54
  @io.puts " stack:"
51
55
  stack.each do |loc|
@@ -59,7 +63,15 @@ module Vivarium
59
63
  event: tp.event.to_s,
60
64
  path: tp.path,
61
65
  lineno: tp.lineno,
62
- events: events.map { |e| { ktime_ns: e.ktime_ns, pid: e.pid, event_name: e.event_name, payload: Vivarium.render_event_payload(e) } },
66
+ events: events.map do |e|
67
+ {
68
+ ktime_ns: e.ktime_ns,
69
+ pid: e.pid,
70
+ severity: (e.respond_to?(:severity) ? e.severity : Vivarium.event_severity(e.event_name)),
71
+ event_name: e.event_name,
72
+ payload: Vivarium.render_event_payload(e)
73
+ }
74
+ end,
63
75
  stack: stack.map { |loc| "#{loc.path}:#{loc.lineno}:in #{loc.base_label}" }
64
76
  }
65
77
  @io.puts JSON.generate(entry)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vivarium
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/vivarium.rb CHANGED
@@ -22,12 +22,56 @@ module Vivarium
22
22
  EVENT_NAME_SIZE = 16
23
23
  EVENT_PAYLOAD_SIZE = 256
24
24
  EVENT_TS_SIZE = 8
25
+ PROC_EXEC_SLOT_SIZE = 64
26
+ PROC_EXEC_SLOT_COUNT = 4
25
27
  EVENT_STRUCT_SIZE = 288
26
28
  EVENT_TS_OFFSET = 0
27
29
  EVENT_PID_OFFSET = 8
28
30
  EVENT_NAME_OFFSET = 12
29
31
  EVENT_PAYLOAD_OFFSET = 28
30
32
  EVENT_CAPACITY = 1024
33
+ EVENT_SEVERITY_HIGH = %w[
34
+ capable_check bprm_creds setid_change task_kill
35
+ ptrace_check sb_mount kernel_read_file
36
+ ].freeze
37
+
38
+ CAPABILITY_NAMES = {
39
+ 0 => "CAP_CHOWN",
40
+ 1 => "CAP_DAC_OVERRIDE",
41
+ 2 => "CAP_DAC_READ_SEARCH",
42
+ 3 => "CAP_FOWNER",
43
+ 4 => "CAP_FSETID",
44
+ 5 => "CAP_KILL",
45
+ 6 => "CAP_SETGID",
46
+ 7 => "CAP_SETUID",
47
+ 8 => "CAP_SETPCAP",
48
+ 9 => "CAP_LINUX_IMMUTABLE",
49
+ 10 => "CAP_NET_BIND_SERVICE",
50
+ 12 => "CAP_NET_ADMIN",
51
+ 13 => "CAP_NET_RAW",
52
+ 16 => "CAP_SYS_MODULE",
53
+ 17 => "CAP_SYS_RAWIO",
54
+ 18 => "CAP_SYS_CHROOT",
55
+ 19 => "CAP_SYS_PTRACE",
56
+ 21 => "CAP_SYS_ADMIN",
57
+ 22 => "CAP_SYS_BOOT",
58
+ 23 => "CAP_SYS_NICE",
59
+ 24 => "CAP_SYS_RESOURCE",
60
+ 25 => "CAP_SYS_TIME",
61
+ 27 => "CAP_MKNOD",
62
+ 29 => "CAP_AUDIT_WRITE",
63
+ 37 => "CAP_AUDIT_READ",
64
+ 38 => "CAP_PERFMON",
65
+ 39 => "CAP_BPF",
66
+ 40 => "CAP_CHECKPOINT_RESTORE"
67
+ }.freeze
68
+
69
+ SETID_FLAG_NAMES = {
70
+ 0x01 => "LSM_SETID_ID",
71
+ 0x02 => "LSM_SETID_RE",
72
+ 0x04 => "LSM_SETID_RES",
73
+ 0x08 => "LSM_SETID_FS"
74
+ }.freeze
31
75
 
32
76
  @bpf_pin_dir = PIN_DIR
33
77
 
@@ -44,6 +88,10 @@ module Vivarium
44
88
  ktime_ns.to_i.zero? && pid.to_i.zero? && event_name.to_s.empty? && payload.to_s.empty?
45
89
  end
46
90
 
91
+ def severity
92
+ Vivarium.event_severity(event_name)
93
+ end
94
+
47
95
  def self.from_binary(raw)
48
96
  bytes = raw.to_s.b
49
97
  bytes = bytes.ljust(EVENT_STRUCT_SIZE, "\x00")
@@ -52,7 +100,13 @@ module Vivarium
52
100
  pid = bytes[EVENT_PID_OFFSET, 4].unpack1("L<")
53
101
  event_name = c_string(bytes[EVENT_NAME_OFFSET, EVENT_NAME_SIZE])
54
102
  raw_payload = bytes[EVENT_PAYLOAD_OFFSET, EVENT_PAYLOAD_SIZE]
55
- payload = if %w[dns_req sock_connect odd_socket].include?(event_name)
103
+ raw_payload_events = %w[
104
+ dns_req sock_connect odd_socket proc_exec
105
+ file_symlink file_hardlink file_rename file_chmod file_getdents
106
+ ptrace_check sb_mount kernel_read_file task_kill
107
+ setid_change capable_check bprm_creds
108
+ ]
109
+ payload = if raw_payload_events.include?(event_name)
56
110
  raw_payload
57
111
  else
58
112
  c_string(raw_payload)
@@ -70,6 +124,14 @@ module Vivarium
70
124
  end
71
125
  end
72
126
 
127
+ def self.c_string(bytes)
128
+ Event.c_string(bytes)
129
+ end
130
+
131
+ def self.event_severity(event_name)
132
+ EVENT_SEVERITY_HIGH.include?(event_name.to_s) ? "high" : "medium"
133
+ end
134
+
73
135
  def self.decode_dns_qname(raw_payload)
74
136
  bytes = raw_payload.to_s.b.bytes
75
137
  labels = []
@@ -143,6 +205,131 @@ module Vivarium
143
205
  decode_odd_socket_payload(raw_payload)
144
206
  end
145
207
 
208
+ def self.decode_file_symlink_payload(raw_payload)
209
+ bytes = raw_payload.to_s.b
210
+ target = c_string(bytes[0, 128])
211
+ link_name = c_string(bytes[128, 128])
212
+ "target=#{target.inspect} link_name=#{link_name.inspect}"
213
+ end
214
+
215
+ def self.decode_file_hardlink_payload(raw_payload)
216
+ bytes = raw_payload.to_s.b
217
+ old_path = c_string(bytes[0, 128])
218
+ new_name = c_string(bytes[128, 128])
219
+ "old_path=#{old_path.inspect} new_name=#{new_name.inspect}"
220
+ end
221
+
222
+ def self.decode_file_rename_payload(raw_payload)
223
+ bytes = raw_payload.to_s.b
224
+ old_name = c_string(bytes[0, 128])
225
+ new_name = c_string(bytes[128, 128])
226
+ "old_name=#{old_name.inspect} new_name=#{new_name.inspect}"
227
+ end
228
+
229
+ def self.decode_file_chmod_payload(raw_payload)
230
+ bytes = raw_payload.to_s.b
231
+ return "" if bytes.bytesize < 2
232
+
233
+ mode = bytes[0, 2].unpack1("S<")
234
+ path = c_string(bytes[2, 254])
235
+ "mode=#{format('0o%o', mode)} path=#{path.inspect}"
236
+ end
237
+
238
+ def self.decode_file_getdents_payload(raw_payload)
239
+ bytes = raw_payload.to_s.b
240
+ return "" if bytes.bytesize < 8
241
+
242
+ fd = bytes[0, 4].unpack1("L<")
243
+ count = bytes[4, 4].unpack1("L<")
244
+ "fd=#{fd} count=#{count}"
245
+ end
246
+
247
+ def self.decode_proc_exec_payload(raw_payload)
248
+ bytes = raw_payload.to_s.b
249
+ slots = PROC_EXEC_SLOT_COUNT.times.map do |index|
250
+ offset = index * PROC_EXEC_SLOT_SIZE
251
+ c_string(bytes[offset, PROC_EXEC_SLOT_SIZE])
252
+ end
253
+ slots.reject!(&:empty?)
254
+ return "" if slots.empty?
255
+
256
+ filename = slots.shift
257
+ argv = slots
258
+ "filename=#{filename.inspect} argv=[#{argv.map(&:inspect).join(', ')}]"
259
+ end
260
+
261
+ def self.decode_ptrace_check_payload(raw_payload)
262
+ bytes = raw_payload.to_s.b
263
+ return "" if bytes.bytesize < 4
264
+
265
+ mode = bytes[0, 4].unpack1("L<")
266
+ "mode=0x#{mode.to_s(16)}"
267
+ end
268
+
269
+ def self.decode_sb_mount_payload(raw_payload)
270
+ bytes = raw_payload.to_s.b
271
+ return "" if bytes.bytesize < 248
272
+
273
+ flags = bytes[0, 8].unpack1("Q<")
274
+ dev_name = c_string(bytes[8, 120])
275
+ fs_type = c_string(bytes[128, 120])
276
+ "flags=0x#{flags.to_s(16)} dev_name=#{dev_name.inspect} fs_type=#{fs_type.inspect}"
277
+ end
278
+
279
+ def self.decode_kernel_read_file_payload(raw_payload)
280
+ bytes = raw_payload.to_s.b
281
+ return "" if bytes.bytesize < 8
282
+
283
+ id = bytes[0, 4].unpack1("L<")
284
+ contents = bytes[4, 4].unpack1("L<")
285
+ "id=#{id} contents=#{contents}"
286
+ end
287
+
288
+ def self.decode_task_kill_payload(raw_payload)
289
+ bytes = raw_payload.to_s.b
290
+ return "" if bytes.bytesize < 4
291
+
292
+ sig = bytes[0, 4].unpack1("l<")
293
+ signame = begin
294
+ Signal.signame(sig)
295
+ rescue ArgumentError
296
+ nil
297
+ end
298
+
299
+ signame ? "sig=#{sig} signame=#{signame}" : "sig=#{sig}"
300
+ end
301
+
302
+ def self.decode_setid_change_payload(raw_payload)
303
+ bytes = raw_payload.to_s.b
304
+ return "" if bytes.bytesize < 4
305
+
306
+ flags = bytes[0, 4].unpack1("L<")
307
+ names = SETID_FLAG_NAMES.each_with_object([]) do |(bit, name), acc|
308
+ acc << name if (flags & bit) != 0
309
+ end
310
+ names << "UNKNOWN" if names.empty?
311
+ "flags=0x#{flags.to_s(16)} kinds=[#{names.join(', ')}]"
312
+ end
313
+
314
+ def self.decode_capable_check_payload(raw_payload)
315
+ bytes = raw_payload.to_s.b
316
+ return "" if bytes.bytesize < 8
317
+
318
+ cap = bytes[0, 4].unpack1("L<")
319
+ opts = bytes[4, 4].unpack1("L<")
320
+ cap_name = CAPABILITY_NAMES.fetch(cap, "UNKNOWN")
321
+ "cap=#{cap}(#{cap_name}) opts=0x#{opts.to_s(16)}"
322
+ end
323
+
324
+ def self.decode_bprm_creds_payload(raw_payload)
325
+ bytes = raw_payload.to_s.b
326
+ return "" if bytes.bytesize < 2
327
+
328
+ has_file = bytes.getbyte(0).to_i
329
+ path = c_string(bytes[1, EVENT_PAYLOAD_SIZE - 1])
330
+ "has_file=#{has_file} file=#{path.inspect}"
331
+ end
332
+
146
333
  def self.render_event_payload(event)
147
334
  case event.event_name
148
335
  when "dns_req"
@@ -154,6 +341,45 @@ module Vivarium
154
341
  when "odd_socket"
155
342
  decoded = decode_odd_socket_payload(event.payload)
156
343
  decoded.empty? ? event.payload.inspect : decoded
344
+ when "proc_exec"
345
+ decoded = decode_proc_exec_payload(event.payload)
346
+ decoded.empty? ? event.payload.inspect : decoded
347
+ when "ptrace_check"
348
+ decoded = decode_ptrace_check_payload(event.payload)
349
+ decoded.empty? ? event.payload.inspect : decoded
350
+ when "sb_mount"
351
+ decoded = decode_sb_mount_payload(event.payload)
352
+ decoded.empty? ? event.payload.inspect : decoded
353
+ when "kernel_read_file"
354
+ decoded = decode_kernel_read_file_payload(event.payload)
355
+ decoded.empty? ? event.payload.inspect : decoded
356
+ when "task_kill"
357
+ decoded = decode_task_kill_payload(event.payload)
358
+ decoded.empty? ? event.payload.inspect : decoded
359
+ when "setid_change"
360
+ decoded = decode_setid_change_payload(event.payload)
361
+ decoded.empty? ? event.payload.inspect : decoded
362
+ when "capable_check"
363
+ decoded = decode_capable_check_payload(event.payload)
364
+ decoded.empty? ? event.payload.inspect : decoded
365
+ when "bprm_creds"
366
+ decoded = decode_bprm_creds_payload(event.payload)
367
+ decoded.empty? ? event.payload.inspect : decoded
368
+ when "file_symlink"
369
+ decoded = decode_file_symlink_payload(event.payload)
370
+ decoded.empty? ? event.payload.inspect : decoded
371
+ when "file_hardlink"
372
+ decoded = decode_file_hardlink_payload(event.payload)
373
+ decoded.empty? ? event.payload.inspect : decoded
374
+ when "file_rename"
375
+ decoded = decode_file_rename_payload(event.payload)
376
+ decoded.empty? ? event.payload.inspect : decoded
377
+ when "file_chmod"
378
+ decoded = decode_file_chmod_payload(event.payload)
379
+ decoded.empty? ? event.payload.inspect : decoded
380
+ when "file_getdents"
381
+ decoded = decode_file_getdents_payload(event.payload)
382
+ decoded.empty? ? event.payload.inspect : decoded
157
383
  else
158
384
  event.payload.inspect
159
385
  end
@@ -249,6 +475,11 @@ module Vivarium
249
475
  struct net;
250
476
  struct sock;
251
477
  struct sk_buff;
478
+ struct task_struct;
479
+ struct kernel_siginfo;
480
+ struct cred;
481
+ struct user_namespace;
482
+ struct linux_binprm;
252
483
 
253
484
  struct path {
254
485
  void *mnt;
@@ -259,6 +490,24 @@ module Vivarium
259
490
  struct path f_path;
260
491
  };
261
492
 
493
+ struct qstr {
494
+ union {
495
+ struct {
496
+ u64 hash_len;
497
+ };
498
+ struct {
499
+ u32 hash;
500
+ u32 len;
501
+ };
502
+ };
503
+ const unsigned char *name;
504
+ };
505
+
506
+ struct dentry_base {
507
+ char __pad[__VIVARIUM_DENTRY_D_NAME_OFFSET__];
508
+ struct qstr d_name;
509
+ };
510
+
262
511
  struct sockaddr_t {
263
512
  u16 sa_family;
264
513
  unsigned char sa_data[14];
@@ -341,6 +590,29 @@ module Vivarium
341
590
  return 0;
342
591
  }
343
592
 
593
+ static __always_inline int monitored_capability(int cap)
594
+ {
595
+ switch (cap) {
596
+ case 1: /* CAP_DAC_OVERRIDE */
597
+ case 2: /* CAP_DAC_READ_SEARCH */
598
+ case 6: /* CAP_SETGID */
599
+ case 7: /* CAP_SETUID */
600
+ case 12: /* CAP_NET_ADMIN */
601
+ case 16: /* CAP_SYS_MODULE */
602
+ case 17: /* CAP_SYS_RAWIO */
603
+ case 19: /* CAP_SYS_PTRACE */
604
+ case 21: /* CAP_SYS_ADMIN */
605
+ case 22: /* CAP_SYS_BOOT */
606
+ case 25: /* CAP_SYS_TIME */
607
+ case 38: /* CAP_PERFMON */
608
+ case 39: /* CAP_BPF */
609
+ case 40: /* CAP_CHECKPOINT_RESTORE */
610
+ return 1;
611
+ default:
612
+ return 0;
613
+ }
614
+ }
615
+
344
616
  static __always_inline void submit_event(struct event_t *ev)
345
617
  {
346
618
  u32 zero = 0;
@@ -396,6 +668,29 @@ module Vivarium
396
668
  submit_event(&ev);
397
669
  }
398
670
 
671
+ static __always_inline int read_dentry_name(struct dentry *dentry, char *buffer, size_t max)
672
+ {
673
+ struct dentry_base d = {};
674
+ struct qstr qname = {};
675
+
676
+ if (!dentry || !buffer) {
677
+ return -1;
678
+ }
679
+
680
+ bpf_probe_read_kernel(&d, sizeof(d), (void *)dentry);
681
+ if (!d.d_name.name) {
682
+ return -1;
683
+ }
684
+
685
+ unsigned int len = d.d_name.len;
686
+ if (len > max) {
687
+ len = max;
688
+ }
689
+
690
+ bpf_probe_read_kernel_str(buffer, len + 1, (void *)d.d_name.name);
691
+ return len;
692
+ }
693
+
399
694
  TRACEPOINT_PROBE(sched, sched_process_fork)
400
695
  {
401
696
  u32 parent = args->parent_pid;
@@ -630,6 +925,343 @@ module Vivarium
630
925
 
631
926
  return 0;
632
927
  }
928
+
929
+ TRACEPOINT_PROBE(syscalls, sys_enter_execve)
930
+ {
931
+ u64 pid_tgid = bpf_get_current_pid_tgid();
932
+ u32 pid = pid_tgid >> 32;
933
+ u32 tid = (u32)pid_tgid;
934
+ const char *argv0 = 0;
935
+ const char *argv1 = 0;
936
+ const char *argv2 = 0;
937
+
938
+ if (!target_enabled(pid, tid)) {
939
+ return 0;
940
+ }
941
+
942
+ if (!args->filename) {
943
+ return 0;
944
+ }
945
+
946
+ struct event_t ev = {};
947
+ ev.pid = pid;
948
+ __builtin_memcpy(ev.event_name, "proc_exec", 10);
949
+ bpf_probe_read_user_str(&ev.payload[0], #{PROC_EXEC_SLOT_SIZE}, args->filename);
950
+
951
+ if (args->argv) {
952
+ bpf_probe_read_user(&argv0, sizeof(argv0), &args->argv[0]);
953
+ bpf_probe_read_user(&argv1, sizeof(argv1), &args->argv[1]);
954
+ bpf_probe_read_user(&argv2, sizeof(argv2), &args->argv[2]);
955
+
956
+ if (argv0) {
957
+ bpf_probe_read_user_str(&ev.payload[#{PROC_EXEC_SLOT_SIZE}], #{PROC_EXEC_SLOT_SIZE}, argv0);
958
+ }
959
+ if (argv1) {
960
+ bpf_probe_read_user_str(&ev.payload[#{PROC_EXEC_SLOT_SIZE * 2}], #{PROC_EXEC_SLOT_SIZE}, argv1);
961
+ }
962
+ if (argv2) {
963
+ bpf_probe_read_user_str(&ev.payload[#{PROC_EXEC_SLOT_SIZE * 3}], #{PROC_EXEC_SLOT_SIZE}, argv2);
964
+ }
965
+ }
966
+
967
+ submit_event(&ev);
968
+ return 0;
969
+ }
970
+
971
+ LSM_PROBE(ptrace_access_check, struct task_struct *child, unsigned int mode)
972
+ {
973
+ u64 pid_tgid = bpf_get_current_pid_tgid();
974
+ u32 pid = pid_tgid >> 32;
975
+ u32 tid = (u32)pid_tgid;
976
+
977
+ if (!target_enabled(pid, tid)) {
978
+ return 0;
979
+ }
980
+
981
+ struct event_t ev = {};
982
+ u32 mode32 = mode;
983
+
984
+ ev.pid = pid;
985
+ __builtin_memcpy(ev.event_name, "ptrace_check", 13);
986
+ __builtin_memcpy(&ev.payload[0], &mode32, sizeof(mode32));
987
+ submit_event(&ev);
988
+
989
+ return 0;
990
+ }
991
+
992
+ LSM_PROBE(sb_mount, const char *dev_name, const struct path *path, const char *type, unsigned long flags, void *data)
993
+ {
994
+ u64 pid_tgid = bpf_get_current_pid_tgid();
995
+ u32 pid = pid_tgid >> 32;
996
+ u32 tid = (u32)pid_tgid;
997
+
998
+ if (!target_enabled(pid, tid)) {
999
+ return 0;
1000
+ }
1001
+
1002
+ struct event_t ev = {};
1003
+ u64 flags64 = flags;
1004
+
1005
+ ev.pid = pid;
1006
+ __builtin_memcpy(ev.event_name, "sb_mount", 9);
1007
+ __builtin_memcpy(&ev.payload[0], &flags64, sizeof(flags64));
1008
+
1009
+ if (dev_name) {
1010
+ bpf_probe_read_kernel_str(&ev.payload[8], 120, dev_name);
1011
+ }
1012
+ if (type) {
1013
+ bpf_probe_read_kernel_str(&ev.payload[128], 120, type);
1014
+ }
1015
+
1016
+ submit_event(&ev);
1017
+
1018
+ return 0;
1019
+ }
1020
+
1021
+ LSM_PROBE(kernel_read_file, struct file *file, int id, int contents)
1022
+ {
1023
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1024
+ u32 pid = pid_tgid >> 32;
1025
+ u32 tid = (u32)pid_tgid;
1026
+
1027
+ if (!target_enabled(pid, tid)) {
1028
+ return 0;
1029
+ }
1030
+
1031
+ struct event_t ev = {};
1032
+ u32 id32 = id;
1033
+ u32 contents32 = contents;
1034
+
1035
+ ev.pid = pid;
1036
+ __builtin_memcpy(ev.event_name, "kernel_read_file", 16);
1037
+ __builtin_memcpy(&ev.payload[0], &id32, sizeof(id32));
1038
+ __builtin_memcpy(&ev.payload[4], &contents32, sizeof(contents32));
1039
+ submit_event(&ev);
1040
+
1041
+ return 0;
1042
+ }
1043
+
1044
+ LSM_PROBE(task_kill, struct task_struct *p, struct kernel_siginfo *info, int sig, const struct cred *cred)
1045
+ {
1046
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1047
+ u32 pid = pid_tgid >> 32;
1048
+ u32 tid = (u32)pid_tgid;
1049
+
1050
+ if (!target_enabled(pid, tid)) {
1051
+ return 0;
1052
+ }
1053
+
1054
+ struct event_t ev = {};
1055
+
1056
+ ev.pid = pid;
1057
+ __builtin_memcpy(ev.event_name, "task_kill", 10);
1058
+ __builtin_memcpy(&ev.payload[0], &sig, sizeof(sig));
1059
+ submit_event(&ev);
1060
+
1061
+ return 0;
1062
+ }
1063
+
1064
+ LSM_PROBE(task_fix_setuid, struct cred *new, const struct cred *old, int flags)
1065
+ {
1066
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1067
+ u32 pid = pid_tgid >> 32;
1068
+ u32 tid = (u32)pid_tgid;
1069
+
1070
+ if (!target_enabled(pid, tid)) {
1071
+ return 0;
1072
+ }
1073
+
1074
+ struct event_t ev = {};
1075
+ u32 flags32 = flags;
1076
+
1077
+ ev.pid = pid;
1078
+ __builtin_memcpy(ev.event_name, "setid_change", 13);
1079
+ __builtin_memcpy(&ev.payload[0], &flags32, sizeof(flags32));
1080
+ submit_event(&ev);
1081
+
1082
+ return 0;
1083
+ }
1084
+
1085
+ LSM_PROBE(capable, const struct cred *cred, struct user_namespace *targ_ns, int cap, unsigned int opts)
1086
+ {
1087
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1088
+ u32 pid = pid_tgid >> 32;
1089
+ u32 tid = (u32)pid_tgid;
1090
+
1091
+ if (!target_enabled(pid, tid)) {
1092
+ return 0;
1093
+ }
1094
+
1095
+ if (!monitored_capability(cap)) {
1096
+ return 0;
1097
+ }
1098
+
1099
+ struct event_t ev = {};
1100
+ u32 cap32 = cap;
1101
+ u32 opts32 = opts;
1102
+
1103
+ ev.pid = pid;
1104
+ __builtin_memcpy(ev.event_name, "capable_check", 14);
1105
+ __builtin_memcpy(&ev.payload[0], &cap32, sizeof(cap32));
1106
+ __builtin_memcpy(&ev.payload[4], &opts32, sizeof(opts32));
1107
+ submit_event(&ev);
1108
+
1109
+ return 0;
1110
+ }
1111
+
1112
+ LSM_PROBE(bprm_creds_from_file, struct linux_binprm *bprm, struct file *file)
1113
+ {
1114
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1115
+ u32 pid = pid_tgid >> 32;
1116
+ u32 tid = (u32)pid_tgid;
1117
+
1118
+ if (!target_enabled(pid, tid)) {
1119
+ return 0;
1120
+ }
1121
+
1122
+ struct event_t ev = {};
1123
+ u8 has_file = 0;
1124
+
1125
+ ev.pid = pid;
1126
+ __builtin_memcpy(ev.event_name, "bprm_creds", 11);
1127
+
1128
+ if (file) {
1129
+ has_file = 1;
1130
+ bpf_d_path(&file->f_path, &ev.payload[1], sizeof(ev.payload) - 1);
1131
+ }
1132
+
1133
+ __builtin_memcpy(&ev.payload[0], &has_file, sizeof(has_file));
1134
+ submit_event(&ev);
1135
+
1136
+ return 0;
1137
+ }
1138
+
1139
+ LSM_PROBE(inode_symlink, struct inode *dir, struct dentry *dentry, const char *oldname)
1140
+ {
1141
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1142
+ u32 pid = pid_tgid >> 32;
1143
+ u32 tid = (u32)pid_tgid;
1144
+
1145
+ if (!target_enabled(pid, tid)) {
1146
+ return 0;
1147
+ }
1148
+
1149
+ struct event_t ev = {};
1150
+ ev.pid = pid;
1151
+ __builtin_memcpy(ev.event_name, "file_symlink", 13);
1152
+
1153
+ if (oldname) {
1154
+ bpf_probe_read_user_str(&ev.payload[0], 128, oldname);
1155
+ }
1156
+
1157
+ if (dentry) {
1158
+ read_dentry_name(dentry, &ev.payload[128], 128);
1159
+ }
1160
+
1161
+ submit_event(&ev);
1162
+ return 0;
1163
+ }
1164
+
1165
+ LSM_PROBE(inode_link, struct dentry *old_dentry, struct inode *dir, struct dentry *new_dentry)
1166
+ {
1167
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1168
+ u32 pid = pid_tgid >> 32;
1169
+ u32 tid = (u32)pid_tgid;
1170
+
1171
+ if (!target_enabled(pid, tid)) {
1172
+ return 0;
1173
+ }
1174
+
1175
+ struct event_t ev = {};
1176
+ ev.pid = pid;
1177
+ __builtin_memcpy(ev.event_name, "file_hardlink", 14);
1178
+
1179
+ if (old_dentry) {
1180
+ read_dentry_name(old_dentry, &ev.payload[0], 128);
1181
+ }
1182
+
1183
+ if (new_dentry) {
1184
+ read_dentry_name(new_dentry, &ev.payload[128], 128);
1185
+ }
1186
+
1187
+ submit_event(&ev);
1188
+ return 0;
1189
+ }
1190
+
1191
+ LSM_PROBE(inode_rename, struct inode *old_dir, struct dentry *old_dentry,
1192
+ struct inode *new_dir, struct dentry *new_dentry, unsigned int flags)
1193
+ {
1194
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1195
+ u32 pid = pid_tgid >> 32;
1196
+ u32 tid = (u32)pid_tgid;
1197
+
1198
+ if (!target_enabled(pid, tid)) {
1199
+ return 0;
1200
+ }
1201
+
1202
+ struct event_t ev = {};
1203
+ ev.pid = pid;
1204
+ __builtin_memcpy(ev.event_name, "file_rename", 12);
1205
+
1206
+ if (old_dentry) {
1207
+ read_dentry_name(old_dentry, &ev.payload[0], 128);
1208
+ }
1209
+
1210
+ if (new_dentry) {
1211
+ read_dentry_name(new_dentry, &ev.payload[128], 128);
1212
+ }
1213
+
1214
+ submit_event(&ev);
1215
+ return 0;
1216
+ }
1217
+
1218
+ LSM_PROBE(path_chmod, struct path *path, umode_t mode)
1219
+ {
1220
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1221
+ u32 pid = pid_tgid >> 32;
1222
+ u32 tid = (u32)pid_tgid;
1223
+
1224
+ if (!target_enabled(pid, tid)) {
1225
+ return 0;
1226
+ }
1227
+
1228
+ if (!path) {
1229
+ return 0;
1230
+ }
1231
+
1232
+ struct event_t ev = {};
1233
+ u16 mode_short = mode & 0xFFFF;
1234
+ ev.pid = pid;
1235
+ __builtin_memcpy(ev.event_name, "file_chmod", 11);
1236
+ __builtin_memcpy(&ev.payload[0], &mode_short, sizeof(mode_short));
1237
+
1238
+ bpf_d_path(path, &ev.payload[2], sizeof(ev.payload) - 2);
1239
+ submit_event(&ev);
1240
+ return 0;
1241
+ }
1242
+
1243
+ TRACEPOINT_PROBE(syscalls, sys_enter_getdents64)
1244
+ {
1245
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1246
+ u32 pid = pid_tgid >> 32;
1247
+ u32 tid = (u32)pid_tgid;
1248
+
1249
+ if (!target_enabled(pid, tid)) {
1250
+ return 0;
1251
+ }
1252
+
1253
+ struct event_t ev = {};
1254
+ u32 fd = args->fd;
1255
+ u32 count = args->count;
1256
+
1257
+ ev.pid = pid;
1258
+ __builtin_memcpy(ev.event_name, "file_getdents", 14);
1259
+ __builtin_memcpy(&ev.payload[0], &fd, sizeof(fd));
1260
+ __builtin_memcpy(&ev.payload[4], &count, sizeof(count));
1261
+
1262
+ submit_event(&ev);
1263
+ return 0;
1264
+ }
633
1265
  CLANG
634
1266
 
635
1267
  def initialize(pin_dir: Vivarium.bpf_pin_dir)
@@ -641,7 +1273,10 @@ module Vivarium
641
1273
  FileUtils.mkdir_p(@pin_dir)
642
1274
 
643
1275
  f_path_offset = detect_f_path_offset
644
- program = BPF_PROGRAM_TEMPLATE.gsub("__VIVARIUM_F_PATH_OFFSET__", f_path_offset.to_s)
1276
+ d_name_offset = detect_dentry_d_name_offset
1277
+ program = BPF_PROGRAM_TEMPLATE
1278
+ .gsub("__VIVARIUM_F_PATH_OFFSET__", f_path_offset.to_s)
1279
+ .gsub("__VIVARIUM_DENTRY_D_NAME_OFFSET__", d_name_offset.to_s)
645
1280
 
646
1281
  bpf = RbBCC::BCC.new(text: program)
647
1282
  kprint_thread = start_kprint_logger(bpf)
@@ -777,6 +1412,56 @@ module Vivarium
777
1412
  rescue Errno::ENOENT
778
1413
  raise Error, "bpftool is required to resolve struct file::f_path offset"
779
1414
  end
1415
+
1416
+ def detect_dentry_d_name_offset
1417
+ env_offset = ENV["VIVARIUM_DENTRY_D_NAME_OFFSET"]
1418
+ return Integer(env_offset, 10) if env_offset
1419
+
1420
+ raw = IO.popen(
1421
+ %w[bpftool btf dump file /sys/kernel/btf/vmlinux format raw],
1422
+ err: IO::NULL,
1423
+ &:read
1424
+ )
1425
+
1426
+ in_dentry_struct = false
1427
+ d_name_bits_offset = nil
1428
+
1429
+ raw.each_line do |line|
1430
+ if line =~ /^\[\d+\] STRUCT 'dentry' /
1431
+ in_dentry_struct = true
1432
+ next
1433
+ end
1434
+
1435
+ if in_dentry_struct && line.start_with?("[")
1436
+ break
1437
+ end
1438
+
1439
+ next unless in_dentry_struct
1440
+
1441
+ if (match = line.match(/'d_name'.*bits_offset=(\d+)/))
1442
+ d_name_bits_offset = Integer(match[1], 10)
1443
+ break
1444
+ end
1445
+ end
1446
+
1447
+ if d_name_bits_offset
1448
+ if (d_name_bits_offset % 8).positive?
1449
+ raise Error, "unsupported d_name bits offset=#{d_name_bits_offset}"
1450
+ end
1451
+
1452
+ if d_name_bits_offset >= 1024
1453
+ warn "[vivariumd] suspicious d_name offset=#{d_name_bits_offset / 8}, fallback to offset=32"
1454
+ return 32
1455
+ end
1456
+
1457
+ return d_name_bits_offset / 8
1458
+ end
1459
+
1460
+ warn "[vivariumd] could not find struct dentry::d_name in BTF, fallback to offset=32"
1461
+ 32
1462
+ rescue Errno::ENOENT
1463
+ raise Error, "bpftool is required to resolve struct dentry::d_name offset"
1464
+ end
780
1465
  end
781
1466
 
782
1467
  class ObservationSession
data/sig/vivarium.rbs CHANGED
@@ -11,6 +11,7 @@ module Vivarium
11
11
  attr_accessor payload: String?
12
12
 
13
13
  def empty?: bool
14
+ def severity: () -> String
14
15
  def self.from_binary: (String raw) -> Event
15
16
  end
16
17
 
@@ -41,6 +42,8 @@ module Vivarium
41
42
  EVENT_NAME_SIZE: Integer
42
43
  EVENT_PAYLOAD_SIZE: Integer
43
44
  EVENT_TS_SIZE: Integer
45
+ PROC_EXEC_SLOT_SIZE: Integer
46
+ PROC_EXEC_SLOT_COUNT: Integer
44
47
  EVENT_STRUCT_SIZE: Integer
45
48
  EVENT_TS_OFFSET: Integer
46
49
  EVENT_PID_OFFSET: Integer
@@ -50,10 +53,24 @@ module Vivarium
50
53
 
51
54
  def self.bpf_pin_dir: () -> String
52
55
  def self.bpf_pin_dir=: (String dir) -> String
56
+ def self.event_severity: (String event_name) -> String
53
57
  def self.decode_dns_qname: (String raw_payload) -> String
54
58
  def self.decode_sock_connect_payload: (String raw_payload) -> String
55
59
  def self.decode_odd_socket_payload: (String raw_payload) -> String
56
60
  def self.decode_bad_socket_payload: (String raw_payload) -> String
61
+ def self.decode_proc_exec_payload: (String raw_payload) -> String
62
+ def self.decode_ptrace_check_payload: (String raw_payload) -> String
63
+ def self.decode_sb_mount_payload: (String raw_payload) -> String
64
+ def self.decode_kernel_read_file_payload: (String raw_payload) -> String
65
+ def self.decode_task_kill_payload: (String raw_payload) -> String
66
+ def self.decode_setid_change_payload: (String raw_payload) -> String
67
+ def self.decode_capable_check_payload: (String raw_payload) -> String
68
+ def self.decode_bprm_creds_payload: (String raw_payload) -> String
69
+ def self.decode_file_symlink_payload: (String raw_payload) -> String
70
+ def self.decode_file_hardlink_payload: (String raw_payload) -> String
71
+ def self.decode_file_rename_payload: (String raw_payload) -> String
72
+ def self.decode_file_chmod_payload: (String raw_payload) -> String
73
+ def self.decode_file_getdents_payload: (String raw_payload) -> String
57
74
  def self.render_event_payload: (Event event) -> String
58
75
  def self.observe: (?pin_dir: String pin_dir, ?logger: untyped logger, ?dest: untyped dest, ?format: Symbol format) { () -> untyped } -> untyped
59
76
  def self.top_observe: (?pin_dir: String pin_dir, ?logger: untyped logger, ?dest: untyped dest, ?format: Symbol format) -> ObservationSession
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vivarium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uchio Kondo
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: 0.11.4
26
+ - !ruby/object:Gem::Dependency
27
+ name: ostruct
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  description: Vivarium visualizes low-level events such as file open paths and relates
27
41
  them to Ruby method boundaries by combining RbBCC (eBPF LSM) and TracePoint.
28
42
  email:
@@ -34,7 +48,11 @@ extra_rdoc_files: []
34
48
  files:
35
49
  - README.md
36
50
  - Rakefile
51
+ - examples/execve_demo.rb
52
+ - examples/file_operation_demo.rb
37
53
  - examples/network_client_demo.rb
54
+ - examples/privilege_event_demo.rb
55
+ - examples/signal_kill_demo.rb
38
56
  - exe/vivariumd
39
57
  - lib/vivarium.rb
40
58
  - lib/vivarium/logger.rb