vivarium 0.3.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8bbb6affa1c85f3c5af82f751021cc6d0230741545bcb090a3441ae6285ddae
4
- data.tar.gz: 0a51c1fd7065cf030363679f8f1c2370937365b0d22115326948cabb6053ded4
3
+ metadata.gz: 150b5b98d7555e1954b0002ca2224b1e445b799fa5d163ab4bbcbc8cac42ea74
4
+ data.tar.gz: 5984b875d3fab3600c86f9e1d961a0c1ab84945f1c1ea76e78e72248ec2fd9f7
5
5
  SHA512:
6
- metadata.gz: 4efc19c34686b652e07213be2c6d865fa9b02fbe0a207b1f9e391cfd8591269f8dafbe6ad7c444091541bd1cb10e2c8a6ae07564caa5fe5deb2ed38226b3a38c
7
- data.tar.gz: e0f15247ae4ed3ff159935686affd554626eb079bda7cf1db9e5f577df93db5ab57134c87ec926cab96da8302990c7524f187bbf120559b386cc40fe8399807f
6
+ metadata.gz: 17847e5f3852628e8bc58f4a31fc4e934b0b06d35d7961ac811d27d34ffcb4a8f17fd9e9c7f61253b9e7d37db09ee9d06354be637f8953c4bd1827baf8d3e0e7
7
+ data.tar.gz: 1980ac521ccd83eadb68bbbb5b6cb6a6053f3737c2b8c7aaeded5b9ace3563b34666da472bfe133204caf5889a4e77febea2c9069900e4f040a728a6f609b75a
data/README.md CHANGED
@@ -21,6 +21,7 @@ Implemented in this repository:
21
21
  - BPF LSM hooks on `inode_symlink`, `inode_link`, `inode_rename`, `path_chmod`
22
22
  - BPF tracepoint on `sys_enter_getdents64`
23
23
  - BPF tracepoint on `sys_enter_execve` (captures executable path and first few argv entries as `proc_exec`)
24
+ - BPF tracepoint on `sched_process_fork` (tracks descendants and emits `proc_fork`)
24
25
  - BPF LSM hooks for suspicious behavior checks:
25
26
  - `ptrace_access_check` (emits `ptrace_check`)
26
27
  - `sb_mount` (emits `sb_mount`)
@@ -32,17 +33,18 @@ Implemented in this repository:
32
33
  - BPF LSM hook on `socket_create` (flags unusual socket creation as `odd_socket`)
33
34
  - BPF LSM hook on `socket_connect` (captures destination family/address/port as `sock_connect`)
34
35
  - BPF tracepoints on `sys_enter_sendmsg`, `sys_enter_sendto`, `sys_enter_sendmmsg` (capture UDP/53 DNS QNAME raw bytes as `dns_req`)
36
+ - USDT probes for method span boundaries and exceptions (`span_start`, `span_stop`, `span_raise`)
37
+ - OpenSSL `SSL_write` uprobe event (`ssl_write`)
35
38
  - Shared pinned maps on bpffs
36
39
  - `config_root_targets` (root PID -> 0/1)
37
40
  - `config_spawned_targets` (spawned TID -> 0/1)
38
- - `event_invoked` (array length 1024 with `event_t` records)
39
- - `event_write_pos` (cursor for appending into `event_invoked`)
41
+ - `events` (`BPF_RINGBUF_OUTPUT`, shared by system events and span events)
40
42
  - Ruby API `Vivarium.observe do ... end`
41
43
  - Registers current PID to `config_root_targets`
42
44
  - eBPF tracks spawned descendants into `config_spawned_targets` via `sched_process_fork`
43
- - On each `:return` / `:c_return`, drains `event_invoked`
44
- - Prints stack trace + events
45
- - Clears event slots and cursor
45
+ - `TracePoint` emits span probes on allowlisted call/return and emits `span_raise` on Ruby `:raise`
46
+ - Correlator thread consumes ringbuf events and joins them to spans by `tid`/time window
47
+ - Renders a process tree once at session end
46
48
  - Unregisters PID on block exit
47
49
 
48
50
  `event_t` currently:
@@ -51,6 +53,7 @@ Implemented in this repository:
51
53
  struct event_t {
52
54
  u64 ktime_ns;
53
55
  u32 pid;
56
+ u32 tid;
54
57
  char event_name[16];
55
58
  char payload[256];
56
59
  };
@@ -147,7 +150,7 @@ observer = Vivarium.top_observe
147
150
  observer.stop
148
151
  ```
149
152
 
150
- By default, Vivarium excludes its own internal frames from stack output. Set `VIVARIUM_FILTER_INTERNAL_FRAMES=0` to disable this filter.
153
+ `Vivarium.observe` / `Vivarium.top_observe` produce one process-tree report at session end (block exit, `observer.stop`, or process exit).
151
154
 
152
155
  You can override pin directory via `VIVARIUM_BPF_PIN_DIR` on both sides:
153
156
 
@@ -179,17 +182,18 @@ bundle exec vivariumd --pin-dir /sys/fs/bpf/vivarium
179
182
  ## Notes
180
183
 
181
184
  - Thread/Ractor-awareness is not yet implemented.
182
- - `event_invoked` uses fixed 1024 slots and wraps around when full.
185
+ - Current transport is ring buffer (`events`) pinned under bpffs.
186
+ - Ring buffer is single-consumer by nature; v1 supports a single observer per host.
183
187
  - `payload` is 256 bytes in `event_t`; some event types intentionally use smaller structured slices inside that buffer.
184
188
  - `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.
189
+ - `span_raise` is emitted on Ruby `:raise` and rendered as `EXCP` lines within the enclosing span.
190
+ - Events that do not match any real span are grouped into synthetic `<no-span>` spans.
185
191
  - 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
192
  - `capable_check` is intentionally filtered to high-risk capabilities to reduce noise from extremely frequent `capable` hook calls.
188
- - Current output format is textual and intended for iteration.
193
+ - Output format is textual process tree with a session header and per-span relative timing.
189
194
  - `vivariumd` resolves `struct file::f_path` offset from `/sys/kernel/btf/vmlinux` at startup.
190
195
  - `vivariumd` also resolves `struct dentry::d_name` offset from `/sys/kernel/btf/vmlinux` at startup.
191
196
  - You can override offsets manually with `VIVARIUM_FILE_F_PATH_OFFSET` and `VIVARIUM_DENTRY_D_NAME_OFFSET` if auto-detection fails.
192
- - `vivariumd` also prints `bpf_trace_printk` lines (`vivarium: pid=... path=...`) to its own logs.
193
197
 
194
198
  ## Contributing
195
199
 
@@ -9,6 +9,9 @@ require "vivarium"
9
9
  # 2) Run this script: bundle exec ruby examples/execve_demo.rb
10
10
 
11
11
  TMP_PREFIX = "vivarium-exec-demo"
12
+ FILTER = {
13
+ include_events: %w[proc_exec]
14
+ }.freeze
12
15
 
13
16
  def try_step(title)
14
17
  puts "[exec-demo] #{title}"
@@ -20,7 +23,7 @@ end
20
23
  Dir.mktmpdir(TMP_PREFIX, "/tmp") do |dir|
21
24
  output_path = File.join(dir, "execve-demo.out")
22
25
 
23
- Vivarium.observe do
26
+ Vivarium.observe(filter: FILTER) do
24
27
  try_step("system echo with multiple args") do
25
28
  system("/bin/echo", "hello", "from", "vivarium", out: File::NULL)
26
29
  end
@@ -10,6 +10,9 @@ require "vivarium"
10
10
  # 2) Run this script: bundle exec ruby examples/file_operation_demo.rb
11
11
 
12
12
  TMP_PREFIX = "vivarium-file-demo"
13
+ FILTER = {
14
+ include_events: %w[path_open file_symlink file_hardlink file_rename file_chmod file_getdents]
15
+ }.freeze
13
16
 
14
17
  def try_step(title)
15
18
  puts "[file-demo] #{title}"
@@ -24,7 +27,7 @@ Dir.mktmpdir(TMP_PREFIX, "/tmp") do |dir|
24
27
  hardlink_path = File.join(dir, "hardlink.txt")
25
28
  symlink_path = File.join(dir, "symlink.txt")
26
29
 
27
- Vivarium.observe do
30
+ Vivarium.observe(filter: FILTER) do
28
31
  try_step("create source file") do
29
32
  File.write(source_path, "vivarium sample\n")
30
33
  File.read(source_path)
@@ -4,6 +4,10 @@
4
4
  require "socket"
5
5
  require "vivarium"
6
6
 
7
+ FILTER = {
8
+ include_events: %w[sock_connect dns_req odd_socket]
9
+ }.freeze
10
+
7
11
  # Usage:
8
12
  # 1) In another shell (root): sudo bundle exec vivariumd
9
13
  # 2) Run this script: bundle exec ruby examples/network_client_demo.rb
@@ -15,7 +19,7 @@ rescue StandardError => e
15
19
  puts "[client] #{title} failed: #{e.class}: #{e.message}"
16
20
  end
17
21
 
18
- Vivarium.observe do
22
+ Vivarium.observe(filter: FILTER) do
19
23
  # Likely emits sock_connect and dns_req via resolver traffic.
20
24
  try_step("system: DNS lookup") do
21
25
  system("getent hosts example.com >/dev/null 2>&1 || true")
@@ -3,6 +3,10 @@
3
3
 
4
4
  require "vivarium"
5
5
 
6
+ FILTER = {
7
+ include_events: %w[setid_change capable_check bprm_creds]
8
+ }.freeze
9
+
6
10
  # Usage:
7
11
  # 1) In another shell (root): sudo bundle exec vivariumd
8
12
  # 2) Run this script: bundle exec ruby examples/privilege_event_demo.rb
@@ -14,7 +18,7 @@ rescue StandardError => e
14
18
  puts "[priv-demo] #{title} failed: #{e.class}: #{e.message}"
15
19
  end
16
20
 
17
- Vivarium.observe do
21
+ Vivarium.observe(filter: FILTER) do
18
22
  try_step("attempt setuid(0)") do
19
23
  Process::UID.change_privilege(0)
20
24
  end
@@ -3,6 +3,10 @@
3
3
 
4
4
  require "vivarium"
5
5
 
6
+ FILTER = {
7
+ include_events: %w[span_raise]
8
+ }.freeze
9
+
6
10
  def try_step(title)
7
11
  puts "[priv-demo] #{title}"
8
12
  yield
@@ -10,7 +14,7 @@ rescue StandardError => e
10
14
  puts "[priv-demo] #{title} failed: #{e.class}: #{e.message}"
11
15
  end
12
16
 
13
- Vivarium.observe do
17
+ Vivarium.observe(filter: FILTER) do
14
18
  try_step("raise in main") do
15
19
  raise "error in main"
16
20
  end
@@ -3,6 +3,10 @@
3
3
 
4
4
  require "vivarium"
5
5
 
6
+ FILTER = {
7
+ include_events: %w[task_kill]
8
+ }.freeze
9
+
6
10
  # Usage:
7
11
  # 1) In another shell (root): sudo bundle exec vivariumd
8
12
  # 2) Run this script: bundle exec ruby examples/signal_kill_demo.rb
@@ -16,7 +20,7 @@ end
16
20
 
17
21
  child_pid = nil
18
22
 
19
- Vivarium.observe do
23
+ Vivarium.observe(filter: FILTER) do
20
24
  try_step("fork child process") do
21
25
  child_pid = fork do
22
26
  trap("TERM") { exit!(0) }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "net/http"
5
+ require "uri"
6
+ require "vivarium"
7
+
8
+ FILTER = {
9
+ include_events: %w[ssl_write]
10
+ }.freeze
11
+
12
+ # Usage:
13
+ # 1) In another shell (root): sudo bundle exec vivariumd
14
+ # (SSL_write uprobe is attached automatically when libssl is found)
15
+ # 2) Run this script: bundle exec ruby examples/ssl_write_demo.rb
16
+ #
17
+ # You can disable the SSL_write uprobe with `sudo vivariumd --no-ssl-trace`
18
+ # or point at a specific library with `sudo vivariumd --libssl /path/to/libssl.so.3`.
19
+
20
+ Vivarium.observe(filter: FILTER) do
21
+ # Net::HTTP uses libssl's SSL_write under the hood. With HTTP/1.1 the
22
+ # request line should appear verbatim in the SSL event payload.
23
+ begin
24
+ uri = URI("https://udzura.jp/")
25
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
26
+ http.get(uri.request_uri)
27
+ end
28
+ rescue StandardError => e
29
+ warn "[ssl_demo] Net::HTTP failed: #{e.class}: #{e.message}"
30
+ end
31
+
32
+ # `curl --http2` should produce HTTP/2 traffic; HEADERS frames will be
33
+ # HPACK-decoded if the `http-2` gem is installed on the observer side.
34
+ system("curl -sS --http2 -o /dev/null https://nghttp2.org/ 2>/dev/null")
35
+ end
36
+
37
+ puts "[ssl_demo] done"
@@ -22,11 +22,12 @@ module Vivarium
22
22
 
23
23
  POLL_TIMEOUT_MS = 200
24
24
 
25
- def initialize(pin_dir:, observer_pid:, main_tid:, method_id_queue:, dest: $stdout)
25
+ def initialize(pin_dir:, observer_pid:, main_tid:, method_id_queue:, filter: nil, dest: $stdout)
26
26
  @pin_dir = pin_dir
27
27
  @observer_pid = observer_pid
28
28
  @main_tid = main_tid
29
29
  @method_id_queue = method_id_queue
30
+ @filter = filter
30
31
  @dest = dest
31
32
 
32
33
  @events = []
@@ -79,6 +80,7 @@ module Vivarium
79
80
  session_start_ktime: @session_start_ktime,
80
81
  session_stop_iso: @session_stop_iso,
81
82
  session_stop_ktime: @session_stop_ktime,
83
+ filter: @filter,
82
84
  dest: @dest
83
85
  ).render
84
86
  end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Vivarium
6
+ class DisplayFilter
7
+ attr_reader :include_events, :exclude_events, :include_severities, :include_pids, :include_tids
8
+
9
+ def self.compile(raw)
10
+ return new if raw.nil?
11
+ return raw if raw.is_a?(self)
12
+
13
+ unless raw.respond_to?(:to_h)
14
+ raise ArgumentError, "filter must be a Hash-compatible object"
15
+ end
16
+
17
+ new(raw.to_h)
18
+ end
19
+
20
+ def initialize(raw = {})
21
+ @raw = symbolize_keys(raw || {})
22
+
23
+ @include_events = normalize_string_set(fetch_key(:include_events, :event_names, :events))
24
+ @exclude_events = normalize_string_set(fetch_key(:exclude_events))
25
+ @include_severities = normalize_string_set(fetch_key(:include_severities, :severities, :severity))
26
+ @include_pids = normalize_integer_set(fetch_key(:include_pids, :pids, :pid))
27
+ @include_tids = normalize_integer_set(fetch_key(:include_tids, :tids, :tid))
28
+
29
+ @include_span_names = normalize_string_set(fetch_key(:include_span_names, :span_names))
30
+ @span_pattern = normalize_pattern(fetch_key(:span, :span_pattern))
31
+
32
+ payload_value = fetch_key(:payload)
33
+ @payload_pattern = normalize_pattern(fetch_key(:payload_pattern))
34
+ @payload_patterns_by_event = {}
35
+ if payload_value.is_a?(Hash)
36
+ @payload_patterns_by_event = normalize_payload_map(payload_value)
37
+ else
38
+ @payload_pattern ||= normalize_pattern(payload_value)
39
+ end
40
+ end
41
+
42
+ def enabled?
43
+ !@raw.empty?
44
+ end
45
+
46
+ def needs_payload?
47
+ !@payload_pattern.nil? || !@payload_patterns_by_event.empty?
48
+ end
49
+
50
+ def allow_span_name?(span_name)
51
+ return true if @include_span_names.empty? && @span_pattern.nil?
52
+
53
+ name = span_name.to_s
54
+ return true if @include_span_names.include?(name)
55
+ return true if @span_pattern && @span_pattern.match?(name)
56
+
57
+ false
58
+ end
59
+
60
+ def allow_event?(event_name:, severity:, span_name:, payload: nil, pid: nil, tid: nil)
61
+ return false unless allow_span_name?(span_name)
62
+
63
+ name = event_name.to_s
64
+ sev = severity.to_s
65
+
66
+ return false if @exclude_events.include?(name)
67
+ return false if !@include_events.empty? && !@include_events.include?(name)
68
+ return false if !@include_severities.empty? && !@include_severities.include?(sev)
69
+ return false if !@include_pids.empty? && !@include_pids.include?(pid.to_i)
70
+ return false if !@include_tids.empty? && !@include_tids.include?(tid.to_i)
71
+
72
+ payload_pattern = @payload_patterns_by_event[name] || @payload_pattern
73
+ if payload_pattern
74
+ return false if payload.nil?
75
+ return false unless payload_pattern.match?(payload.to_s)
76
+ end
77
+
78
+ true
79
+ end
80
+
81
+ private
82
+
83
+ def fetch_key(*keys)
84
+ keys.each do |key|
85
+ return @raw[key] if @raw.key?(key)
86
+ end
87
+ nil
88
+ end
89
+
90
+ def symbolize_keys(hash)
91
+ hash.each_with_object({}) do |(k, v), out|
92
+ out[k.respond_to?(:to_sym) ? k.to_sym : k] = v
93
+ end
94
+ end
95
+
96
+ def normalize_string_set(value)
97
+ arr = case value
98
+ when nil
99
+ []
100
+ when Array
101
+ value
102
+ else
103
+ [value]
104
+ end
105
+
106
+ arr.each_with_object(Set.new) do |item, set|
107
+ str = item.to_s.strip
108
+ set << str unless str.empty?
109
+ end
110
+ end
111
+
112
+ def normalize_integer_set(value)
113
+ arr = case value
114
+ when nil
115
+ []
116
+ when Array
117
+ value
118
+ else
119
+ [value]
120
+ end
121
+
122
+ arr.each_with_object(Set.new) do |item, set|
123
+ begin
124
+ set << Integer(item)
125
+ rescue ArgumentError, TypeError
126
+ nil
127
+ end
128
+ end
129
+ end
130
+
131
+ def normalize_pattern(value)
132
+ case value
133
+ when nil
134
+ nil
135
+ when Regexp
136
+ value
137
+ when String
138
+ return nil if value.empty?
139
+
140
+ Regexp.new(Regexp.escape(value))
141
+ else
142
+ nil
143
+ end
144
+ end
145
+
146
+ def normalize_payload_map(raw_map)
147
+ raw_map.each_with_object({}) do |(event_name, pattern), out|
148
+ key = event_name.to_s.strip
149
+ next if key.empty?
150
+
151
+ normalized = normalize_pattern(pattern)
152
+ next unless normalized
153
+
154
+ out[key] = normalized
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "http/2"
5
+ rescue LoadError
6
+ # http/2 gem is optional; without it we can still parse frame headers but not HPACK-decompress HEADERS.
7
+ end
8
+
9
+ module Vivarium
10
+ # Decodes payloads captured from OpenSSL `SSL_write` into a human-readable
11
+ # one-liner. Auto-detects HTTP/1.x request/response lines and HTTP/2 binary
12
+ # frames; HPACK-decompresses HEADERS / CONTINUATION when the `http-2` gem
13
+ # is available, otherwise reports frame types only.
14
+ #
15
+ # HPACK decompressor state is kept per pid. This is sufficient for the
16
+ # common "one HTTPS connection per process" case; with multiple concurrent
17
+ # TLS connections per pid the HPACK table can diverge and decoding may
18
+ # fail — in that case the decompressor for that pid is reset on the next
19
+ # decode error.
20
+ class HttpDecoder
21
+ HTTP1_METHODS = %w[GET POST PUT PATCH DELETE HEAD OPTIONS TRACE CONNECT].freeze
22
+ HTTP2_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".b
23
+ H2_FRAME_HEADER_SIZE = 9
24
+ H2_FLAG_END_HEADERS = 0x04
25
+ H2_FLAG_PADDED = 0x08
26
+ H2_FLAG_PRIORITY = 0x20
27
+ FRAME_TYPE_NAMES = {
28
+ 0x0 => "DATA",
29
+ 0x1 => "HEADERS",
30
+ 0x2 => "PRIORITY",
31
+ 0x3 => "RST_STREAM",
32
+ 0x4 => "SETTINGS",
33
+ 0x5 => "PUSH_PROMISE",
34
+ 0x6 => "PING",
35
+ 0x7 => "GOAWAY",
36
+ 0x8 => "WINDOW_UPDATE",
37
+ 0x9 => "CONTINUATION"
38
+ }.freeze
39
+
40
+ def initialize
41
+ @hpack_available = load_http2_gem
42
+ @decompressors = {}
43
+ @continuation = {}
44
+ end
45
+
46
+ def hpack_available?
47
+ @hpack_available
48
+ end
49
+
50
+ def render(pid:, data:, data_len:)
51
+ data = data.to_s.b
52
+ data_len = data_len.to_i
53
+
54
+ return "data_len=0" if data_len <= 0
55
+ return "len=#{data_len} <no-capture>" if data.empty?
56
+
57
+ if (summary = http1_summary(data))
58
+ kind, line = summary
59
+ return "http/1.x #{kind}: #{line}#{truncation_note(data, data_len)}"
60
+ end
61
+
62
+ rest = data
63
+ preface_note = ""
64
+ if rest.start_with?(HTTP2_PREFACE)
65
+ preface_note = "preface "
66
+ rest = rest.byteslice(HTTP2_PREFACE.bytesize..) || "".b
67
+ end
68
+
69
+ frames = parse_h2_frames(rest)
70
+ if frames.empty?
71
+ if !preface_note.empty?
72
+ return "h2 preface only#{truncation_note(data, data_len)}"
73
+ end
74
+ return "binary len=#{data_len}#{truncation_note(data, data_len)}"
75
+ end
76
+
77
+ rendered = frames.map { |f| render_h2_frame(pid, f) }.join(" | ")
78
+ "h2 #{preface_note}#{rendered}#{truncation_note(data, data_len)}"
79
+ end
80
+
81
+ private
82
+
83
+ def truncation_note(data, data_len)
84
+ return "" if data.bytesize >= data_len
85
+
86
+ " (captured #{data.bytesize}/#{data_len}B)"
87
+ end
88
+
89
+ def load_http2_gem
90
+ require "http/2"
91
+ true
92
+ rescue LoadError
93
+ false
94
+ end
95
+
96
+ def http1_summary(data)
97
+ head = data.byteslice(0, 512).to_s
98
+ first_line = head.split("\r\n", 2).first
99
+ return nil if first_line.nil? || first_line.empty?
100
+
101
+ first_line = first_line.dup.force_encoding(Encoding::UTF_8)
102
+ return nil unless first_line.valid_encoding?
103
+
104
+ if HTTP1_METHODS.any? { |m| first_line.start_with?("#{m} ") }
105
+ return ["request", first_line]
106
+ end
107
+
108
+ if first_line.start_with?("HTTP/1.1 ") || first_line.start_with?("HTTP/1.0 ")
109
+ return ["response", first_line]
110
+ end
111
+
112
+ nil
113
+ end
114
+
115
+ # @return [Array<Array(Integer, Integer, Integer, String, Boolean)>]
116
+ # each entry: [frame_type, flags, stream_id, frame_payload, truncated?]
117
+ def parse_h2_frames(payload)
118
+ frames = []
119
+ i = 0
120
+ total = payload.bytesize
121
+
122
+ while i + H2_FRAME_HEADER_SIZE <= total
123
+ length = (payload.getbyte(i) << 16) |
124
+ (payload.getbyte(i + 1) << 8) |
125
+ payload.getbyte(i + 2)
126
+ frame_type = payload.getbyte(i + 3)
127
+ flags = payload.getbyte(i + 4)
128
+ stream_id = payload.byteslice(i + 5, 4).unpack1("N") & 0x7fff_ffff
129
+ i += H2_FRAME_HEADER_SIZE
130
+
131
+ if i + length > total
132
+ remaining = payload.byteslice(i, total - i) || "".b
133
+ frames << [frame_type, flags, stream_id, remaining, true]
134
+ break
135
+ end
136
+
137
+ frame_payload = payload.byteslice(i, length) || "".b
138
+ i += length
139
+ frames << [frame_type, flags, stream_id, frame_payload, false]
140
+ end
141
+
142
+ # Heuristic: if the very first "frame" doesn't look like a valid HTTP/2
143
+ # frame, refuse the whole parse so we fall back to "binary".
144
+ first_type = frames.first && frames.first[0]
145
+ return [] if first_type && !FRAME_TYPE_NAMES.key?(first_type)
146
+
147
+ frames
148
+ end
149
+
150
+ def render_h2_frame(pid, frame)
151
+ frame_type, flags, stream_id, frame_payload, truncated = frame
152
+ frame_name = FRAME_TYPE_NAMES.fetch(frame_type, "TYPE0x#{format('%02x', frame_type)}")
153
+ header = "#{frame_name} stream=#{stream_id} flags=0x#{format('%02x', flags)} len=#{frame_payload.bytesize}#{truncated ? '*' : ''}"
154
+
155
+ case frame_type
156
+ when 0x1 # HEADERS
157
+ fragment = headers_fragment(flags, frame_payload)
158
+ return "#{header} <bad_payload>" if fragment.nil?
159
+
160
+ if (flags & H2_FLAG_END_HEADERS) != 0
161
+ pseudo = decode_hpack(pid, fragment)
162
+ "#{header}#{format_pseudo(pseudo)}"
163
+ else
164
+ @continuation[[pid, stream_id]] = fragment.dup
165
+ "#{header} <collecting>"
166
+ end
167
+ when 0x9 # CONTINUATION
168
+ key = [pid, stream_id]
169
+ unless @continuation.key?(key)
170
+ return "#{header} <orphan>"
171
+ end
172
+
173
+ @continuation[key] << frame_payload
174
+ if (flags & H2_FLAG_END_HEADERS) == 0
175
+ "#{header} <collecting>"
176
+ else
177
+ buf = @continuation.delete(key)
178
+ pseudo = decode_hpack(pid, buf)
179
+ "#{header}#{format_pseudo(pseudo)}"
180
+ end
181
+ else
182
+ header
183
+ end
184
+ end
185
+
186
+ def headers_fragment(flags, frame_payload)
187
+ start_idx = 0
188
+ end_idx = frame_payload.bytesize
189
+
190
+ if (flags & H2_FLAG_PADDED) != 0
191
+ return nil if end_idx.zero?
192
+
193
+ pad_len = frame_payload.getbyte(0)
194
+ start_idx += 1
195
+ end_idx = [start_idx, end_idx - pad_len].max
196
+ end
197
+
198
+ if (flags & H2_FLAG_PRIORITY) != 0
199
+ return nil if start_idx + 5 > end_idx
200
+
201
+ start_idx += 5
202
+ end
203
+
204
+ frame_payload.byteslice(start_idx, end_idx - start_idx) || "".b
205
+ end
206
+
207
+ def decompressor_for(pid)
208
+ return nil unless @hpack_available
209
+
210
+ @decompressors[pid] ||= HTTP2::Header::Decompressor.new
211
+ end
212
+
213
+ def decode_hpack(pid, header_block)
214
+ dec = decompressor_for(pid)
215
+ return { ":error" => "hpack-unavailable" } unless dec
216
+
217
+ pairs = dec.decode(header_block.b)
218
+ pairs.each_with_object({}) { |(k, v), h| h[k] = v }
219
+ rescue StandardError => e
220
+ @decompressors.delete(pid)
221
+ { ":error" => "#{e.class}: #{e.message}" }
222
+ end
223
+
224
+ def format_pseudo(pseudo)
225
+ return " <error: #{pseudo[':error']}>" if pseudo.key?(":error")
226
+
227
+ parts = []
228
+ parts << ":method=#{pseudo[':method']}" if pseudo[':method']
229
+ parts << ":path=#{pseudo[':path']}" if pseudo[':path']
230
+ parts << ":authority=#{pseudo[':authority']}" if pseudo[':authority']
231
+ parts << ":status=#{pseudo[':status']}" if pseudo[':status']
232
+ return "" if parts.empty?
233
+
234
+ " #{parts.join(' ')}"
235
+ end
236
+ end
237
+ end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "set"
4
+ require_relative "http_decoder"
4
5
 
5
6
  module Vivarium
6
7
  class TreeRenderer
7
8
  SPAN_EVENT_NAMES = %w[span_start span_stop].to_set.freeze
8
9
  FORK_EVENT_NAME = "proc_fork"
9
10
  EXEC_EVENT_NAME = "proc_exec"
11
+ SSL_WRITE_EVENT_NAME = "ssl_write"
10
12
 
11
13
  LSM_EVENT_NAMES = %w[
12
14
  path_open sock_connect odd_socket
@@ -19,6 +21,8 @@ module Vivarium
19
21
  dns_req proc_exec file_getdents proc_fork
20
22
  ].to_set.freeze
21
23
 
24
+ UPROBE_EVENT_NAMES = %w[ssl_write].to_set.freeze
25
+
22
26
  SYNTHETIC_SPAN_NAME = "<no-span>"
23
27
  UNRESOLVED_METHOD_PREFIX = "<method_id="
24
28
 
@@ -49,7 +53,7 @@ module Vivarium
49
53
 
50
54
  def initialize(events:, method_table:, observer_pid:, main_tid:,
51
55
  session_start_iso:, session_start_ktime:,
52
- session_stop_iso:, session_stop_ktime:, dest:)
56
+ session_stop_iso:, session_stop_ktime:, filter: nil, dest:)
53
57
  @events = events
54
58
  @method_table = method_table
55
59
  @observer_pid = observer_pid
@@ -58,6 +62,7 @@ module Vivarium
58
62
  @session_start_ktime = session_start_ktime
59
63
  @session_stop_iso = session_stop_iso
60
64
  @session_stop_ktime = session_stop_ktime
65
+ @display_filter = Vivarium::DisplayFilter.compile(filter)
61
66
  @dest = dest
62
67
 
63
68
  @pid_comm = { observer_pid => "ruby" }
@@ -265,6 +270,7 @@ module Vivarium
265
270
  @dest.puts "[PROC pid=#{@observer_pid} comm=#{@pid_comm[@observer_pid] || 'ruby'}]"
266
271
  children = spans.reject { |s| s.synthetic && s.events.empty? }
267
272
  .reject { |s| @child_span_set.include?(s) }
273
+ .select { |s| span_visible?(s) }
268
274
  children.each_with_index do |span, idx|
269
275
  print_span(span, prefix: "", is_last: idx == children.size - 1)
270
276
  end
@@ -320,6 +326,14 @@ module Vivarium
320
326
  root_children = []
321
327
 
322
328
  span.events.each do |ev|
329
+ target_text = nil
330
+ if @display_filter.needs_payload?
331
+ target_text = render_target(ev)
332
+ next unless event_visible?(ev, span, target_text)
333
+ else
334
+ next unless event_visible?(ev, span)
335
+ end
336
+
323
337
  if ev.event_name == FORK_EVENT_NAME
324
338
  child_pid = read_proc_fork_child_pid(ev.payload)
325
339
  child_node = ProcNode.new(
@@ -333,7 +347,7 @@ module Vivarium
333
347
  ev_node = EventNode.new(
334
348
  kind: kind_for(ev),
335
349
  name: ev.event_name,
336
- target: render_target(ev),
350
+ target: target_text || render_target(ev),
337
351
  offset_ns: ev.ktime_ns - span.start_ktime,
338
352
  child_proc: child_node
339
353
  )
@@ -344,7 +358,7 @@ module Vivarium
344
358
  ev_node = EventNode.new(
345
359
  kind: kind_for(ev),
346
360
  name: ev.event_name,
347
- target: render_target(ev),
361
+ target: target_text || render_target(ev),
348
362
  offset_ns: ev.ktime_ns - span.start_ktime,
349
363
  child_proc: nil
350
364
  )
@@ -416,8 +430,12 @@ module Vivarium
416
430
  end
417
431
 
418
432
  def print_nodes(nodes, prefix)
419
- nodes.each_with_index do |node, idx|
420
- is_last = idx == nodes.size - 1
433
+ visible_nodes = nodes.select do |node|
434
+ !node.is_a?(Span) || span_visible?(node)
435
+ end
436
+
437
+ visible_nodes.each_with_index do |node, idx|
438
+ is_last = idx == visible_nodes.size - 1
421
439
  case node
422
440
  when EventNode
423
441
  print_event_node(node, prefix: prefix, is_last: is_last)
@@ -429,6 +447,21 @@ module Vivarium
429
447
  end
430
448
  end
431
449
 
450
+ def span_visible?(span)
451
+ @display_filter.allow_span_name?(span_display_name(span))
452
+ end
453
+
454
+ def event_visible?(ev, span, target_text = nil)
455
+ @display_filter.allow_event?(
456
+ event_name: ev.event_name,
457
+ severity: Vivarium.event_severity(ev.event_name),
458
+ span_name: span_display_name(span),
459
+ payload: target_text,
460
+ pid: ev.pid,
461
+ tid: ev.tid
462
+ )
463
+ end
464
+
432
465
  def print_event_node(node, prefix:, is_last:)
433
466
  marker = is_last && node.child_proc.nil? ? "└─ " : "├─ "
434
467
  marker = "└─ " if is_last && node.child_proc.nil?
@@ -457,6 +490,7 @@ module Vivarium
457
490
  def kind_for(ev)
458
491
  return "EXCP" if ev.event_name == "span_raise"
459
492
  return "USDT" if SPAN_EVENT_NAMES.include?(ev.event_name)
493
+ return "SSL" if ev.event_name == SSL_WRITE_EVENT_NAME
460
494
  return "LSM" if LSM_EVENT_NAMES.include?(ev.event_name)
461
495
  return "TP" if TP_EVENT_NAMES.include?(ev.event_name)
462
496
 
@@ -465,12 +499,28 @@ module Vivarium
465
499
 
466
500
  def render_target(ev)
467
501
  return render_raise_target(ev) if ev.event_name == "span_raise"
502
+ return render_ssl_write_target(ev) if ev.event_name == SSL_WRITE_EVENT_NAME
468
503
 
469
504
  text = Vivarium.render_event_payload(ev).to_s
470
505
  text = text.gsub(/\s+/, " ").strip
471
506
  text.empty? ? "-" : text
472
507
  end
473
508
 
509
+ def render_ssl_write_target(ev)
510
+ decoded = Vivarium.decode_ssl_write_payload(ev.payload)
511
+ http_decoder.render(
512
+ pid: ev.pid,
513
+ data: decoded[:data],
514
+ data_len: decoded[:data_len]
515
+ )
516
+ rescue StandardError => e
517
+ "ssl_write <decode-error: #{e.class}: #{e.message}>"
518
+ end
519
+
520
+ def http_decoder
521
+ @http_decoder ||= Vivarium::HttpDecoder.new
522
+ end
523
+
474
524
  def render_raise_target(ev)
475
525
  bytes = ev.payload.to_s.b
476
526
  return "-" if bytes.bytesize < 8
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vivarium
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/vivarium.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "fiddle"
4
4
  require "fileutils"
5
+ require "net/http"
5
6
  require "optparse"
6
7
  require "pathname"
7
8
  require "rbbcc"
@@ -30,6 +31,27 @@ module Vivarium
30
31
  EVENT_NAME_OFFSET = 16
31
32
  EVENT_PAYLOAD_OFFSET = 32
32
33
  EVENTS_RINGBUF_PAGES = 256
34
+
35
+ SSL_WRITE_PAYLOAD_DATA_LEN_OFFSET = 0
36
+ SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET = 4
37
+ SSL_WRITE_PAYLOAD_DATA_OFFSET = 8
38
+ SSL_WRITE_PAYLOAD_DATA_MAX = EVENT_PAYLOAD_SIZE - SSL_WRITE_PAYLOAD_DATA_OFFSET
39
+
40
+ LIBSSL_SEARCH_PATHS = [
41
+ "/lib/x86_64-linux-gnu/libssl.so.3",
42
+ "/lib/x86_64-linux-gnu/libssl.so.1.1",
43
+ "/lib/aarch64-linux-gnu/libssl.so.3",
44
+ "/lib/aarch64-linux-gnu/libssl.so.1.1",
45
+ "/usr/lib/x86_64-linux-gnu/libssl.so.3",
46
+ "/usr/lib/x86_64-linux-gnu/libssl.so.1.1",
47
+ "/usr/lib/aarch64-linux-gnu/libssl.so.3",
48
+ "/usr/lib/aarch64-linux-gnu/libssl.so.1.1",
49
+ "/usr/lib64/libssl.so.3",
50
+ "/usr/lib64/libssl.so.1.1",
51
+ "/usr/lib/libssl.so.3",
52
+ "/usr/lib/libssl.so.1.1"
53
+ ].freeze
54
+
33
55
  SPAN_ALLOWCLASSES = [
34
56
  Socket,
35
57
  BasicSocket,
@@ -43,6 +65,7 @@ module Vivarium
43
65
  Process,
44
66
  Process::UID,
45
67
  Process::GID,
68
+ Net::HTTP,
46
69
  ]
47
70
  SPAN_ALLOWLIST = [
48
71
  "Kernel#system",
@@ -342,6 +365,17 @@ module Vivarium
342
365
  result
343
366
  end
344
367
 
368
+ def self.decode_ssl_write_payload(raw_payload)
369
+ bytes = raw_payload.to_s.b
370
+ return { data_len: 0, cap_len: 0, data: "".b } if bytes.bytesize < SSL_WRITE_PAYLOAD_DATA_OFFSET
371
+
372
+ data_len = bytes[SSL_WRITE_PAYLOAD_DATA_LEN_OFFSET, 4].unpack1("L<")
373
+ cap_len = bytes[SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET, 4].unpack1("L<")
374
+ cap_len = SSL_WRITE_PAYLOAD_DATA_MAX if cap_len > SSL_WRITE_PAYLOAD_DATA_MAX
375
+ data = bytes[SSL_WRITE_PAYLOAD_DATA_OFFSET, cap_len] || "".b
376
+ { data_len: data_len, cap_len: cap_len, data: data }
377
+ end
378
+
345
379
  def self.decode_span_raise_payload(raw_payload)
346
380
  bytes = raw_payload.to_s.b
347
381
  return "" if bytes.bytesize < 8
@@ -426,6 +460,9 @@ module Vivarium
426
460
  when "file_getdents"
427
461
  decoded = decode_file_getdents_payload(event.payload)
428
462
  decoded.empty? ? event.payload.inspect : decoded
463
+ when "ssl_write"
464
+ decoded = decode_ssl_write_payload(event.payload)
465
+ "data_len=#{decoded[:data_len]} cap_len=#{decoded[:cap_len]}"
429
466
  else
430
467
  strip_to_first_null(event.payload).inspect
431
468
  end
@@ -1339,6 +1376,42 @@ module Vivarium
1339
1376
  return 0;
1340
1377
  }
1341
1378
 
1379
+ int on_ssl_write(struct pt_regs *ctx)
1380
+ {
1381
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1382
+ u32 pid = pid_tgid >> 32;
1383
+ u32 tid = (u32)pid_tgid;
1384
+
1385
+ if (!target_enabled(pid, tid)) {
1386
+ return 0;
1387
+ }
1388
+
1389
+ const char *buf = (const char *)PT_REGS_PARM2(ctx);
1390
+ int num = (int)PT_REGS_PARM3(ctx);
1391
+ if (!buf || num <= 0) {
1392
+ return 0;
1393
+ }
1394
+
1395
+ struct event_t ev = {};
1396
+ ev.pid = pid;
1397
+ __builtin_memcpy(ev.event_name, "ssl_write", 10);
1398
+
1399
+ u32 data_len = (u32)num;
1400
+ u32 cap = data_len;
1401
+ if (cap > #{SSL_WRITE_PAYLOAD_DATA_MAX}) {
1402
+ cap = #{SSL_WRITE_PAYLOAD_DATA_MAX};
1403
+ }
1404
+ __builtin_memcpy(&ev.payload[#{SSL_WRITE_PAYLOAD_DATA_LEN_OFFSET}], &data_len, sizeof(data_len));
1405
+ __builtin_memcpy(&ev.payload[#{SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET}], &cap, sizeof(cap));
1406
+ if (bpf_probe_read_user(&ev.payload[#{SSL_WRITE_PAYLOAD_DATA_OFFSET}], cap, buf) < 0) {
1407
+ u32 zero = 0;
1408
+ __builtin_memcpy(&ev.payload[#{SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET}], &zero, sizeof(zero));
1409
+ }
1410
+
1411
+ submit_event(&ev);
1412
+ return 0;
1413
+ }
1414
+
1342
1415
  int on_span_raise(struct pt_regs *ctx)
1343
1416
  {
1344
1417
  u64 pid_tgid = bpf_get_current_pid_tgid();
@@ -1370,8 +1443,10 @@ module Vivarium
1370
1443
  }
1371
1444
  CLANG
1372
1445
 
1373
- def initialize(pin_dir: Vivarium.bpf_pin_dir)
1446
+ def initialize(pin_dir: Vivarium.bpf_pin_dir, ssl_trace: true, libssl_path: nil)
1374
1447
  @pin_dir = pin_dir
1448
+ @ssl_trace = ssl_trace
1449
+ @libssl_path = libssl_path
1375
1450
  end
1376
1451
 
1377
1452
  def run
@@ -1392,6 +1467,8 @@ module Vivarium
1392
1467
 
1393
1468
  bpf = RbBCC::BCC.new(text: program, usdt_contexts: [usdt])
1394
1469
 
1470
+ attach_ssl_write_uprobe(bpf) if @ssl_trace
1471
+
1395
1472
  config_root_targets = bpf["config_root_targets"]
1396
1473
  config_spawned_targets = bpf["config_spawned_targets"]
1397
1474
  events_ringbuf = bpf["events"]
@@ -1416,6 +1493,39 @@ module Vivarium
1416
1493
 
1417
1494
  private
1418
1495
 
1496
+ def attach_ssl_write_uprobe(bpf)
1497
+ path = resolve_libssl_path
1498
+ unless path
1499
+ warn "[vivariumd] libssl not found; SSL_write uprobe disabled " \
1500
+ "(set --libssl PATH or VIVARIUM_LIBSSL_PATH to override)"
1501
+ return
1502
+ end
1503
+
1504
+ bpf.attach_uprobe(name: path, sym: "SSL_write", fn_name: "on_ssl_write")
1505
+ puts "[vivariumd] SSL_write uprobe attached via #{path}"
1506
+ rescue StandardError => e
1507
+ warn "[vivariumd] SSL_write uprobe attach failed: #{e.class}: #{e.message}"
1508
+ end
1509
+
1510
+ def resolve_libssl_path
1511
+ if @libssl_path
1512
+ return @libssl_path if File.exist?(@libssl_path)
1513
+
1514
+ warn "[vivariumd] --libssl path does not exist: #{@libssl_path}"
1515
+ return nil
1516
+ end
1517
+
1518
+ env_path = ENV["VIVARIUM_LIBSSL_PATH"]
1519
+ if env_path && !env_path.empty?
1520
+ return env_path if File.exist?(env_path)
1521
+
1522
+ warn "[vivariumd] VIVARIUM_LIBSSL_PATH does not exist: #{env_path}"
1523
+ return nil
1524
+ end
1525
+
1526
+ LIBSSL_SEARCH_PATHS.find { |p| File.exist?(p) }
1527
+ end
1528
+
1419
1529
  def ensure_root!
1420
1530
  return if Process.uid.zero?
1421
1531
 
@@ -1557,13 +1667,13 @@ module Vivarium
1557
1667
  end
1558
1668
  end
1559
1669
 
1560
- def self.observe(pin_dir: bpf_pin_dir, dest: $stdout, &block)
1561
- return scoped_observe(pin_dir: pin_dir, dest: dest, &block) if block_given?
1670
+ def self.observe(pin_dir: bpf_pin_dir, dest: $stdout, filter: nil, &block)
1671
+ return scoped_observe(pin_dir: pin_dir, dest: dest, filter: filter, &block) if block_given?
1562
1672
 
1563
- top_observe(pin_dir: pin_dir, dest: dest)
1673
+ top_observe(pin_dir: pin_dir, dest: dest, filter: filter)
1564
1674
  end
1565
1675
 
1566
- def self.top_observe(pin_dir: bpf_pin_dir, dest: $stdout)
1676
+ def self.top_observe(pin_dir: bpf_pin_dir, dest: $stdout, filter: nil)
1567
1677
  require "vivarium_usdt"
1568
1678
 
1569
1679
  store = MapStore.new(pin_dir: pin_dir)
@@ -1578,6 +1688,7 @@ module Vivarium
1578
1688
  observer_pid: pid,
1579
1689
  main_tid: main_tid,
1580
1690
  method_id_queue: method_id_queue,
1691
+ filter: filter,
1581
1692
  dest: dest
1582
1693
  )
1583
1694
  correlator.start
@@ -1592,7 +1703,7 @@ module Vivarium
1592
1703
  session
1593
1704
  end
1594
1705
 
1595
- def self.scoped_observe(pin_dir:, dest:)
1706
+ def self.scoped_observe(pin_dir:, dest:, filter: nil)
1596
1707
  require "vivarium_usdt"
1597
1708
 
1598
1709
  store = MapStore.new(pin_dir: pin_dir)
@@ -1607,6 +1718,7 @@ module Vivarium
1607
1718
  observer_pid: pid,
1608
1719
  main_tid: main_tid,
1609
1720
  method_id_queue: method_id_queue,
1721
+ filter: filter,
1610
1722
  dest: dest
1611
1723
  )
1612
1724
  correlator.start
@@ -1626,6 +1738,11 @@ module Vivarium
1626
1738
  allowlist = SPAN_ALLOWLIST
1627
1739
  TracePoint.new(:call, :c_call, :return, :c_return, :raise) do |tp|
1628
1740
  if tp.event == :raise
1741
+ # FIXME: handle threaded events in the future
1742
+ if tp.raised_exception.kind_of?(ThreadError)
1743
+ next
1744
+ end
1745
+
1629
1746
  Vivarium::Usdt.raise(
1630
1747
  tp.raised_exception.class.to_s,
1631
1748
  tp.raised_exception.message.to_s,
@@ -1677,15 +1794,26 @@ module Vivarium
1677
1794
  end
1678
1795
 
1679
1796
  def self.run_daemon!(argv = ARGV)
1680
- options = { pin_dir: bpf_pin_dir }
1797
+ options = { pin_dir: bpf_pin_dir, ssl_trace: true, libssl_path: nil }
1681
1798
  OptionParser.new do |opts|
1682
- opts.banner = "Usage: vivariumd [--pin-dir PATH]"
1799
+ opts.banner = "Usage: vivariumd [--pin-dir PATH] [--no-ssl-trace] [--libssl PATH]"
1683
1800
  opts.on("--pin-dir PATH", "Pinned map directory") { |v| options[:pin_dir] = v }
1801
+ opts.on("--[no-]ssl-trace", "Attach OpenSSL SSL_write uprobe (default: enabled)") do |v|
1802
+ options[:ssl_trace] = v
1803
+ end
1804
+ opts.on("--libssl PATH", "Path to libssl.so to attach SSL_write to") do |v|
1805
+ options[:libssl_path] = v
1806
+ end
1684
1807
  end.parse!(argv)
1685
1808
 
1686
- Daemon.new(pin_dir: options[:pin_dir]).run
1809
+ Daemon.new(
1810
+ pin_dir: options[:pin_dir],
1811
+ ssl_trace: options[:ssl_trace],
1812
+ libssl_path: options[:libssl_path]
1813
+ ).run
1687
1814
  end
1688
1815
  end
1689
1816
 
1690
1817
  require_relative "vivarium/correlator"
1818
+ require_relative "vivarium/display_filter"
1691
1819
  require_relative "vivarium/tree_renderer"
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.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uchio Kondo
@@ -70,6 +70,7 @@ files:
70
70
  - examples/privilege_event_demo.rb
71
71
  - examples/raise_demo.rb
72
72
  - examples/signal_kill_demo.rb
73
+ - examples/ssl_write_demo.rb
73
74
  - examples/sudo_attempt_demo.rb
74
75
  - exe/vivarium
75
76
  - exe/vivariumd
@@ -77,6 +78,8 @@ files:
77
78
  - lib/vivarium.rb
78
79
  - lib/vivarium/cli.rb
79
80
  - lib/vivarium/correlator.rb
81
+ - lib/vivarium/display_filter.rb
82
+ - lib/vivarium/http_decoder.rb
80
83
  - lib/vivarium/tree_renderer.rb
81
84
  - lib/vivarium/version.rb
82
85
  - logo-simple.png