vivarium 0.2.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 +4 -4
- data/CONTEXT.md +535 -0
- data/README.md +16 -12
- data/examples/execve_demo.rb +4 -1
- data/examples/file_operation_demo.rb +4 -1
- data/examples/network_client_demo.rb +5 -1
- data/examples/privilege_event_demo.rb +5 -1
- data/examples/raise_demo.rb +46 -0
- data/examples/signal_kill_demo.rb +5 -1
- data/examples/ssl_write_demo.rb +37 -0
- data/examples/sudo_attempt_demo.rb +18 -0
- data/exe/vivarium +6 -0
- data/image.png +0 -0
- data/lib/vivarium/cli.rb +40 -0
- data/lib/vivarium/correlator.rb +139 -0
- data/lib/vivarium/display_filter.rb +158 -0
- data/lib/vivarium/http_decoder.rb +237 -0
- data/lib/vivarium/tree_renderer.rb +593 -0
- data/lib/vivarium/version.rb +1 -1
- data/lib/vivarium.rb +446 -175
- data/logo-simple.png +0 -0
- metadata +31 -5
- data/lib/vivarium/logger.rb +0 -80
|
@@ -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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "vivarium"
|
|
5
|
+
|
|
6
|
+
FILTER = {
|
|
7
|
+
include_events: %w[span_raise]
|
|
8
|
+
}.freeze
|
|
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(filter: FILTER) do
|
|
18
|
+
try_step("raise in main") do
|
|
19
|
+
raise "error in main"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
try_step("raise in eval") do
|
|
23
|
+
eval("raise 'error in eval'")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
try_step("raise in nested eval") do
|
|
27
|
+
eval(<<~RUBY)
|
|
28
|
+
eval(<<~INNER_RUBY)
|
|
29
|
+
begin
|
|
30
|
+
eval(<<~INNER_INNER_RUBY)
|
|
31
|
+
puts "Hi"
|
|
32
|
+
raise "error in nested nested eval"
|
|
33
|
+
INNER_INNER_RUBY
|
|
34
|
+
rescue StandardError => _
|
|
35
|
+
puts "Rescued in nested eval"
|
|
36
|
+
end
|
|
37
|
+
File.open("/etc/hosts")
|
|
38
|
+
INNER_RUBY
|
|
39
|
+
RUBY
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
try_step("raise in method") do
|
|
44
|
+
File.open("notfound")
|
|
45
|
+
end
|
|
46
|
+
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"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
def debug_output(msg)
|
|
4
|
+
$stderr.puts("[DEBUG] #{msg}") if ENV["VIVARIUM_DEBUG"]
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
debug_output "=== sudo attempt demo ==="
|
|
8
|
+
|
|
9
|
+
debug_output "[1] Attempting: sudo id"
|
|
10
|
+
system("sudo", "-n", "id")
|
|
11
|
+
|
|
12
|
+
debug_output "[2] Attempting: sudo cat /etc/shadow"
|
|
13
|
+
system("sudo", "-n", "cat", "/etc/shadow")
|
|
14
|
+
|
|
15
|
+
debug_output "[3] Attempting: sudo cat /proc/1/environ"
|
|
16
|
+
system("sudo", "-n", "cat", "/proc/1/environ")
|
|
17
|
+
|
|
18
|
+
debug_output "=== done ==="
|
data/exe/vivarium
ADDED
data/image.png
ADDED
|
Binary file
|
data/lib/vivarium/cli.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Vivarium
|
|
6
|
+
module CLI
|
|
7
|
+
def self.run!(argv = ARGV)
|
|
8
|
+
options = { pin_dir: Vivarium.bpf_pin_dir, dest: $stdout }
|
|
9
|
+
parser = OptionParser.new do |opts|
|
|
10
|
+
opts.banner = "Usage: vivarium [options] <command> [args]"
|
|
11
|
+
opts.separator ""
|
|
12
|
+
opts.separator "Commands:"
|
|
13
|
+
opts.separator " load <script> Load and observe a Ruby script"
|
|
14
|
+
opts.separator ""
|
|
15
|
+
opts.separator "Options:"
|
|
16
|
+
opts.on("--pin-dir PATH", "Pinned map directory") { |v| options[:pin_dir] = v }
|
|
17
|
+
opts.on("-o", "--output PATH", "Log output file (default: stdout)") { |v| options[:dest] = File.open(v, "a") }
|
|
18
|
+
end
|
|
19
|
+
parser.order!(argv)
|
|
20
|
+
|
|
21
|
+
command = argv.shift
|
|
22
|
+
case command
|
|
23
|
+
when "load"
|
|
24
|
+
run_load!(argv, options)
|
|
25
|
+
else
|
|
26
|
+
abort parser.help
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.run_load!(argv, options)
|
|
31
|
+
script = argv.shift
|
|
32
|
+
abort "Usage: vivarium load <script>" unless script
|
|
33
|
+
abort "File not found: #{script}" unless File.exist?(script)
|
|
34
|
+
|
|
35
|
+
Vivarium.observe(pin_dir: options[:pin_dir], dest: options[:dest]) do
|
|
36
|
+
Kernel.load(File.expand_path(script))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbbcc"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Vivarium
|
|
7
|
+
class Correlator
|
|
8
|
+
RawEvent = Struct.new(
|
|
9
|
+
:ktime_ns, :pid, :tid, :event_name, :payload,
|
|
10
|
+
keyword_init: true
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
EVENT_C_TYPE = <<~C
|
|
14
|
+
struct event_t {
|
|
15
|
+
u64 ktime_ns;
|
|
16
|
+
u32 pid;
|
|
17
|
+
u32 tid;
|
|
18
|
+
char event_name[16];
|
|
19
|
+
char payload[256];
|
|
20
|
+
};
|
|
21
|
+
C
|
|
22
|
+
|
|
23
|
+
POLL_TIMEOUT_MS = 200
|
|
24
|
+
|
|
25
|
+
def initialize(pin_dir:, observer_pid:, main_tid:, method_id_queue:, filter: nil, dest: $stdout)
|
|
26
|
+
@pin_dir = pin_dir
|
|
27
|
+
@observer_pid = observer_pid
|
|
28
|
+
@main_tid = main_tid
|
|
29
|
+
@method_id_queue = method_id_queue
|
|
30
|
+
@filter = filter
|
|
31
|
+
@dest = dest
|
|
32
|
+
|
|
33
|
+
@events = []
|
|
34
|
+
@events_mutex = Mutex.new
|
|
35
|
+
@method_table = {}
|
|
36
|
+
@stop_flag = false
|
|
37
|
+
@started = false
|
|
38
|
+
|
|
39
|
+
@ringbuf = RbBCC::RingBuf.from_pin(
|
|
40
|
+
File.join(@pin_dir, "events"),
|
|
41
|
+
EVENT_C_TYPE,
|
|
42
|
+
Vivarium::EVENTS_RINGBUF_PAGES
|
|
43
|
+
)
|
|
44
|
+
@ringbuf.open_ring_buffer do |_ctx, data, size|
|
|
45
|
+
capture_event(data, size)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def start
|
|
50
|
+
return if @started
|
|
51
|
+
|
|
52
|
+
@session_start_iso = Time.now.utc.iso8601(3)
|
|
53
|
+
@session_start_ktime = Vivarium.monotonic_ktime_ns
|
|
54
|
+
@thread = Thread.new { run }
|
|
55
|
+
@started = true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def stop
|
|
59
|
+
return unless @started
|
|
60
|
+
return if @stopped
|
|
61
|
+
|
|
62
|
+
@stop_flag = true
|
|
63
|
+
@thread&.join(POLL_TIMEOUT_MS * 4 / 1000.0 + 1)
|
|
64
|
+
@session_stop_iso = Time.now.utc.iso8601(3)
|
|
65
|
+
@session_stop_ktime = Vivarium.monotonic_ktime_ns
|
|
66
|
+
|
|
67
|
+
3.times { safe_poll(50) }
|
|
68
|
+
drain_method_id_queue
|
|
69
|
+
|
|
70
|
+
events_snapshot = @events_mutex.synchronize { @events.dup }
|
|
71
|
+
method_table_snapshot = @method_table.dup
|
|
72
|
+
@stopped = true
|
|
73
|
+
|
|
74
|
+
TreeRenderer.new(
|
|
75
|
+
events: events_snapshot,
|
|
76
|
+
method_table: method_table_snapshot,
|
|
77
|
+
observer_pid: @observer_pid,
|
|
78
|
+
main_tid: @main_tid,
|
|
79
|
+
session_start_iso: @session_start_iso,
|
|
80
|
+
session_start_ktime: @session_start_ktime,
|
|
81
|
+
session_stop_iso: @session_stop_iso,
|
|
82
|
+
session_stop_ktime: @session_stop_ktime,
|
|
83
|
+
filter: @filter,
|
|
84
|
+
dest: @dest
|
|
85
|
+
).render
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def run
|
|
91
|
+
until @stop_flag
|
|
92
|
+
safe_poll(POLL_TIMEOUT_MS)
|
|
93
|
+
drain_method_id_queue
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def safe_poll(timeout_ms)
|
|
98
|
+
@ringbuf.ring_buffer_poll(timeout_ms)
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
warn "[vivarium correlator] poll error: #{e.class}: #{e.message}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def capture_event(data, size)
|
|
104
|
+
bytes = data[0, size].to_s.b
|
|
105
|
+
bytes = bytes.ljust(Vivarium::EVENT_STRUCT_SIZE, "\x00") if bytes.bytesize < Vivarium::EVENT_STRUCT_SIZE
|
|
106
|
+
|
|
107
|
+
ktime_ns = bytes[Vivarium::EVENT_TS_OFFSET, Vivarium::EVENT_TS_SIZE].unpack1("Q<")
|
|
108
|
+
pid = bytes[Vivarium::EVENT_PID_OFFSET, 4].unpack1("L<")
|
|
109
|
+
tid = bytes[Vivarium::EVENT_TID_OFFSET, 4].unpack1("L<")
|
|
110
|
+
event_name = Vivarium.c_string(bytes[Vivarium::EVENT_NAME_OFFSET, Vivarium::EVENT_NAME_SIZE])
|
|
111
|
+
payload = bytes[Vivarium::EVENT_PAYLOAD_OFFSET, Vivarium::EVENT_PAYLOAD_SIZE].to_s.b
|
|
112
|
+
|
|
113
|
+
@events_mutex.synchronize do
|
|
114
|
+
@events << RawEvent.new(
|
|
115
|
+
ktime_ns: ktime_ns,
|
|
116
|
+
pid: pid,
|
|
117
|
+
tid: tid,
|
|
118
|
+
event_name: event_name,
|
|
119
|
+
payload: payload
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
warn "[vivarium correlator] capture error: #{e.class}: #{e.message}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def drain_method_id_queue
|
|
127
|
+
loop do
|
|
128
|
+
msg = begin
|
|
129
|
+
@method_id_queue.pop(true)
|
|
130
|
+
rescue ThreadError
|
|
131
|
+
return
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
method_id, signature = msg
|
|
135
|
+
@method_table[method_id] = signature
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
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
|