straycall 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/Rakefile +8 -0
- data/benchmark/require_overhead.rb +67 -0
- data/docs/index.html +186 -0
- data/docs/styles.css +718 -0
- data/examples/.straycall.yml +13 -0
- data/examples/rspec.rb +13 -0
- data/exe/straycall +6 -0
- data/exe/straycall-supervisor +29 -0
- data/lib/straycall/cli.rb +73 -0
- data/lib/straycall/config.rb +217 -0
- data/lib/straycall/handlers/exec.rb +22 -0
- data/lib/straycall/handlers/filesystem.rb +65 -0
- data/lib/straycall/in_process.rb +74 -0
- data/lib/straycall/minitest.rb +47 -0
- data/lib/straycall/policy/exec.rb +44 -0
- data/lib/straycall/policy/filesystem.rb +119 -0
- data/lib/straycall/policy/network.rb +83 -0
- data/lib/straycall/policy.rb +45 -0
- data/lib/straycall/prompt.rb +24 -0
- data/lib/straycall/recorder.rb +38 -0
- data/lib/straycall/report/console.rb +29 -0
- data/lib/straycall/report/json.rb +19 -0
- data/lib/straycall/reporter/client.rb +102 -0
- data/lib/straycall/reporter/protocol.rb +79 -0
- data/lib/straycall/reporter/symbolizer.rb +26 -0
- data/lib/straycall/reporter/thread_reporter.rb +73 -0
- data/lib/straycall/rspec.rb +51 -0
- data/lib/straycall/supervisor.rb +249 -0
- data/lib/straycall/target.rb +31 -0
- data/lib/straycall/version.rb +5 -0
- data/lib/straycall/violation.rb +9 -0
- data/lib/straycall.rb +39 -0
- metadata +94 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "policy"
|
|
4
|
+
require_relative "report/console"
|
|
5
|
+
require_relative "report/json"
|
|
6
|
+
require_relative "reporter/client"
|
|
7
|
+
require_relative "reporter/symbolizer"
|
|
8
|
+
require_relative "prompt"
|
|
9
|
+
require_relative "handlers/filesystem"
|
|
10
|
+
require_relative "handlers/exec"
|
|
11
|
+
|
|
12
|
+
module Straycall
|
|
13
|
+
class Supervisor
|
|
14
|
+
include Handlers::Filesystem
|
|
15
|
+
include Handlers::Exec
|
|
16
|
+
|
|
17
|
+
ENDPOINT_SOCKET_DOMAINS = [Socket::AF_UNIX, Socket::AF_INET, Socket::AF_INET6].freeze
|
|
18
|
+
MMSGHDR_SIZE = 64
|
|
19
|
+
MAX_MMSG = 1024
|
|
20
|
+
|
|
21
|
+
attr_reader :violations
|
|
22
|
+
|
|
23
|
+
def initialize(config, reporter: nil, console: Report::Console.new, prompt: Prompt.new)
|
|
24
|
+
@config = config
|
|
25
|
+
@reporter = reporter
|
|
26
|
+
@console = console
|
|
27
|
+
@violations = []
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
@reported_locations = {}
|
|
30
|
+
@bound_sockets = {}
|
|
31
|
+
@prompt = prompt
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def run(command)
|
|
35
|
+
supervisor_socket, target_socket = UNIXSocket.pair
|
|
36
|
+
target_socket.close_on_exec = false
|
|
37
|
+
supervisor = Seccomp::Notify.spawn(Policy.seccomp(@config), concurrency: 8) do
|
|
38
|
+
supervisor_socket.close
|
|
39
|
+
environment, argv = command.first.is_a?(Hash) ? [command.first.dup, command.drop(1)] : [{}, command]
|
|
40
|
+
reporter = File.expand_path("target", __dir__)
|
|
41
|
+
inherited_rubyopt = environment.key?("RUBYOPT") ? environment["RUBYOPT"] : ENV["RUBYOPT"]
|
|
42
|
+
environment["STRAYCALL_REPORT_FD"] = target_socket.fileno.to_s
|
|
43
|
+
environment["RUBYOPT"] = ["-r#{reporter}", inherited_rubyopt].compact.join(" ")
|
|
44
|
+
exec(environment, *argv)
|
|
45
|
+
end
|
|
46
|
+
target_socket.close
|
|
47
|
+
@reporter = Reporter::Client.new(supervisor_socket)
|
|
48
|
+
register(supervisor)
|
|
49
|
+
status = supervisor.run
|
|
50
|
+
write_report
|
|
51
|
+
[status, violations]
|
|
52
|
+
ensure
|
|
53
|
+
target_socket&.close unless target_socket&.closed?
|
|
54
|
+
@reporter&.close
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def register(supervisor)
|
|
58
|
+
supervisor.on(:socket) { |request| handle_socket(request) }
|
|
59
|
+
supervisor.on(:connect) { |request| handle_address(request, 1, 2) }
|
|
60
|
+
supervisor.on(:bind) { |request| handle_bind(request) }
|
|
61
|
+
supervisor.on(:listen) { |request| handle_listen(request) }
|
|
62
|
+
supervisor.on(:sendto) { |request| handle_address(request, 4, 5) }
|
|
63
|
+
supervisor.on(:sendmsg) { |request| handle_sendmsg(request) }
|
|
64
|
+
supervisor.on(:sendmmsg) { |request| handle_sendmmsg(request) }
|
|
65
|
+
names = Policy.notification_names(@config)
|
|
66
|
+
supervisor.on(:openat) { |request| handle_openat(request) } if names.include?(:openat)
|
|
67
|
+
supervisor.on(:openat2) { |request| handle_openat2(request) } if names.include?(:openat2)
|
|
68
|
+
supervisor.on(:unlinkat) { |request| handle_path_mutation(request, request.args[0], request.args[1]) } if names.include?(:unlinkat)
|
|
69
|
+
supervisor.on(:mkdirat) { |request| handle_path_mutation(request, request.args[0], request.args[1]) } if names.include?(:mkdirat)
|
|
70
|
+
supervisor.on(:renameat) { |request| handle_rename(request, *request.args.values_at(0, 1, 2, 3)) } if names.include?(:renameat)
|
|
71
|
+
supervisor.on(:renameat2) { |request| handle_rename(request, *request.args.values_at(0, 1, 2, 3)) } if names.include?(:renameat2)
|
|
72
|
+
cwd = Policy::Filesystem::AT_FDCWD
|
|
73
|
+
supervisor.on(:open) { |request| handle_open(request, cwd, request.args[0], request.args[1]) } if names.include?(:open)
|
|
74
|
+
if names.include?(:creat)
|
|
75
|
+
supervisor.on(:creat) do |request|
|
|
76
|
+
flags = Fcntl::O_WRONLY | Fcntl::O_CREAT | Fcntl::O_TRUNC
|
|
77
|
+
handle_open(request, cwd, request.args[0], flags)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
supervisor.on(:unlink) { |request| handle_path_mutation(request, cwd, request.args[0]) } if names.include?(:unlink)
|
|
81
|
+
supervisor.on(:mkdir) { |request| handle_path_mutation(request, cwd, request.args[0]) } if names.include?(:mkdir)
|
|
82
|
+
supervisor.on(:rename) { |request| handle_rename(request, cwd, request.args[0], cwd, request.args[1]) } if names.include?(:rename)
|
|
83
|
+
supervisor.on(:execve) { |request| handle_exec(request, nil, 0) } if names.include?(:execve)
|
|
84
|
+
supervisor.on(:execveat) { |request| handle_exec(request, 0, 1) } if names.include?(:execveat)
|
|
85
|
+
supervisor
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def handle_socket(request)
|
|
91
|
+
domain = request.args[0]
|
|
92
|
+
return request.continue! if @config.network.allow_all? || ENDPOINT_SOCKET_DOMAINS.include?(domain)
|
|
93
|
+
|
|
94
|
+
record_violation(request, {type: "socket", domain:, operation: "socket"}, nil)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def handle_bind(request)
|
|
98
|
+
return request.continue! if @config.network.allow_all? && @config.on_violation != :record
|
|
99
|
+
|
|
100
|
+
address = request.read_sockaddr(request.args[1], request.args[2])
|
|
101
|
+
return request.error!(Errno::EPERM) unless request.valid?
|
|
102
|
+
if @config.network.allowed?(address) && @config.on_violation != :record
|
|
103
|
+
remember_bound_socket(request, address)
|
|
104
|
+
return request.continue!(unsafe: true)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
denied = record_violation(request, target(address), suggestion(address))
|
|
108
|
+
remember_bound_socket(request, address) unless denied
|
|
109
|
+
rescue Seccomp::Notify::MemoryReadError
|
|
110
|
+
record_violation(request, {type: "unknown", host: "?", port: "?"}, nil)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def handle_listen(request)
|
|
114
|
+
return request.continue! if @config.network.allow_all?
|
|
115
|
+
|
|
116
|
+
identity = socket_identity(request.tid, request.args[0])
|
|
117
|
+
address = @mutex.synchronize { @bound_sockets[identity] }
|
|
118
|
+
if address && @config.network.allowed?(address) && @config.on_violation != :record
|
|
119
|
+
return request.continue!
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
listen_target = address ? target(address).merge(operation: "listen") : {type: "socket", fd: request.args[0], operation: "listen"}
|
|
123
|
+
record_violation(request, listen_target, address && suggestion(address))
|
|
124
|
+
rescue SystemCallError
|
|
125
|
+
record_violation(request, {type: "socket", fd: request.args[0], operation: "listen"}, nil)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def handle_address(request, address_arg, length_arg)
|
|
129
|
+
return request.continue! if @config.network.allow_all? && @config.on_violation != :record
|
|
130
|
+
|
|
131
|
+
if request.syscall == :sendto && (request.args[address_arg].zero? || request.args[length_arg].zero?)
|
|
132
|
+
return request.continue!
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
address = request.read_sockaddr(request.args[address_arg], request.args[length_arg])
|
|
136
|
+
return request.error!(Errno::EPERM) unless request.valid?
|
|
137
|
+
if @config.network.allowed?(address) && @config.on_violation != :record
|
|
138
|
+
return request.continue!(unsafe: true)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
record_violation(request, target(address), suggestion(address))
|
|
142
|
+
rescue Seccomp::Notify::MemoryReadError
|
|
143
|
+
record_violation(request, {type: "unknown", host: "?", port: "?"}, nil)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def handle_sendmsg(request)
|
|
147
|
+
return request.continue! if @config.network.allow_all? && @config.on_violation != :record
|
|
148
|
+
|
|
149
|
+
name, length = request.read(request.args[1], 16).unpack("Q<L<")
|
|
150
|
+
return request.continue!(unsafe: true) if name.zero? || length.zero?
|
|
151
|
+
|
|
152
|
+
address = request.read_sockaddr(name, length)
|
|
153
|
+
return request.error!(Errno::EPERM) unless request.valid?
|
|
154
|
+
if @config.network.allowed?(address) && @config.on_violation != :record
|
|
155
|
+
return request.continue!(unsafe: true)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
record_violation(request, target(address), suggestion(address))
|
|
159
|
+
rescue Seccomp::Notify::MemoryReadError
|
|
160
|
+
record_violation(request, {type: "unknown", host: "?", port: "?"}, nil)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def handle_sendmmsg(request)
|
|
164
|
+
return request.continue! if @config.network.allow_all? && @config.on_violation != :record
|
|
165
|
+
|
|
166
|
+
count = [request.args[2], MAX_MMSG].min
|
|
167
|
+
return request.continue! if count.zero?
|
|
168
|
+
|
|
169
|
+
destinations = count.times.filter_map do |index|
|
|
170
|
+
name, length = request.read(request.args[1] + index * MMSGHDR_SIZE, 12).unpack("Q<L<")
|
|
171
|
+
request.read_sockaddr(name, length) unless name.zero? || length.zero?
|
|
172
|
+
end
|
|
173
|
+
return request.error!(Errno::EPERM) unless request.valid?
|
|
174
|
+
observed = if @config.on_violation == :record
|
|
175
|
+
destinations
|
|
176
|
+
else
|
|
177
|
+
destinations.reject { |address| @config.network.allowed?(address) }
|
|
178
|
+
end
|
|
179
|
+
return request.continue!(unsafe: true) if observed.empty?
|
|
180
|
+
return record_violation(request, target(observed.first), suggestion(observed.first)) if @config.on_violation == :fail
|
|
181
|
+
|
|
182
|
+
denied = observed.map do |address|
|
|
183
|
+
record_violation(request, target(address), suggestion(address), respond: false)
|
|
184
|
+
end.any?
|
|
185
|
+
denied ? request.error!(Errno::EPERM) : request.continue!(unsafe: true)
|
|
186
|
+
rescue Seccomp::Notify::MemoryReadError
|
|
187
|
+
record_violation(request, {type: "unknown", host: "?", port: "?"}, nil)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def record_violation(request, target, suggestion, respond: true)
|
|
191
|
+
denied = @config.on_violation == :fail ||
|
|
192
|
+
(@config.on_violation == :prompt && !@prompt.allow?(request.syscall, report_target(target)))
|
|
193
|
+
action = denied ? :denied : :allowed
|
|
194
|
+
location = [request.syscall, request.instruction_pointer, target]
|
|
195
|
+
first = @mutex.synchronize { !@reported_locations.key?(location) && (@reported_locations[location] = true) }
|
|
196
|
+
backtrace = @reporter&.backtrace(request.tid) if first && @config.report_backtrace
|
|
197
|
+
origin = Reporter::Symbolizer.call(request.tid, request.instruction_pointer) unless backtrace
|
|
198
|
+
violation = Violation.new(request.syscall, request.tid, target, backtrace, @reporter&.example, action, suggestion, origin)
|
|
199
|
+
@mutex.synchronize { violations << violation }
|
|
200
|
+
@reporter&.notify_violation if denied
|
|
201
|
+
@mutex.synchronize do
|
|
202
|
+
write_report
|
|
203
|
+
@console.violation(violation)
|
|
204
|
+
end
|
|
205
|
+
denied ? request.error!(Errno::EPERM) : request.continue!(unsafe: true) if respond
|
|
206
|
+
denied
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def target(address)
|
|
210
|
+
return {type: "unix", path: address.unix_path} if address.unix?
|
|
211
|
+
|
|
212
|
+
{type: "inet", host: address.ip_address, port: address.ip_port}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def suggestion(address)
|
|
216
|
+
return %(allow_unix #{address.unix_path.inspect}) if address.unix?
|
|
217
|
+
return %(allow_host #{address.ip_address.inspect}) if address.ip_port.zero?
|
|
218
|
+
|
|
219
|
+
%(allow_host #{address.ip_address.inspect}, ports: [#{address.ip_port}])
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def write_report
|
|
223
|
+
Report::JSON.new(@config.report_path).write(violations) if @config.report_path
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def report_target(target)
|
|
227
|
+
return target[:path] if target[:path]
|
|
228
|
+
return "socket domain #{target[:domain]}" if target[:domain]
|
|
229
|
+
return "socket fd #{target[:fd]}" if target[:type] == "socket"
|
|
230
|
+
|
|
231
|
+
"#{target[:host]}:#{target[:port]}"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def remember_bound_socket(request, address)
|
|
235
|
+
identity = socket_identity(request.tid, request.args[0])
|
|
236
|
+
@mutex.synchronize do
|
|
237
|
+
# ponytail: observations precede the kernel bind result; use pidfd_getfd + getsockname if this becomes a security boundary.
|
|
238
|
+
@bound_sockets[identity] = address if identity
|
|
239
|
+
@bound_sockets.shift if @bound_sockets.length > 4096
|
|
240
|
+
end
|
|
241
|
+
rescue SystemCallError
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def socket_identity(tid, fd)
|
|
246
|
+
File.readlink("/proc/#{tid}/fd/#{fd}")[/\Asocket:\[(\d+)\]\z/, 1]
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "reporter/thread_reporter"
|
|
4
|
+
|
|
5
|
+
module Straycall
|
|
6
|
+
module Target
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def start(socket = nil)
|
|
10
|
+
socket ||= IO.for_fd(Integer(ENV.fetch("STRAYCALL_REPORT_FD")), autoclose: true)
|
|
11
|
+
@reporter = Reporter::ThreadReporter.new(socket).start
|
|
12
|
+
rescue KeyError, ArgumentError
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reporter
|
|
17
|
+
@reporter
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def violation?
|
|
21
|
+
@reporter&.violation? || false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop
|
|
25
|
+
@reporter&.stop
|
|
26
|
+
@reporter = nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Straycall::Target.start if ENV["STRAYCALL_REPORT_FD"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Straycall
|
|
4
|
+
Violation = Struct.new(:syscall, :tid, :target, :backtrace, :example, :action, :suggestion, :origin) do
|
|
5
|
+
def to_h
|
|
6
|
+
{syscall: syscall.to_s, tid:, example:, target:, backtrace:, action: action.to_s, suggestion:, origin:}.compact
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
data/lib/straycall.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "straycall/version"
|
|
4
|
+
require_relative "straycall/config"
|
|
5
|
+
require_relative "straycall/policy"
|
|
6
|
+
require_relative "straycall/violation"
|
|
7
|
+
|
|
8
|
+
module Straycall
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
class ConfigurationError < Error; end
|
|
11
|
+
class PathResolutionError < Error; end
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
attr_writer :config
|
|
15
|
+
|
|
16
|
+
def config
|
|
17
|
+
@config ||= Config.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def configure
|
|
21
|
+
yield config
|
|
22
|
+
config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def available?
|
|
26
|
+
return false if ENV["STRAYCALL"] == "0"
|
|
27
|
+
return false unless RUBY_PLATFORM.include?("linux") && RUBY_ENGINE == "ruby"
|
|
28
|
+
|
|
29
|
+
require "seccomp/notify"
|
|
30
|
+
Seccomp::Notify.supported? && Seccomp::Notify.features[:continue]
|
|
31
|
+
rescue LoadError, StandardError
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reset!
|
|
36
|
+
@config = Config.new
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: straycall
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: seccomp-notify
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.2'
|
|
26
|
+
description: A Linux seccomp-notify sandbox that reports network, filesystem, and
|
|
27
|
+
process violations.
|
|
28
|
+
email:
|
|
29
|
+
- t.yudai92@gmail.com
|
|
30
|
+
executables:
|
|
31
|
+
- straycall
|
|
32
|
+
- straycall-supervisor
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- CHANGELOG.md
|
|
37
|
+
- LICENSE.txt
|
|
38
|
+
- README.md
|
|
39
|
+
- Rakefile
|
|
40
|
+
- benchmark/require_overhead.rb
|
|
41
|
+
- docs/index.html
|
|
42
|
+
- docs/styles.css
|
|
43
|
+
- examples/.straycall.yml
|
|
44
|
+
- examples/rspec.rb
|
|
45
|
+
- exe/straycall
|
|
46
|
+
- exe/straycall-supervisor
|
|
47
|
+
- lib/straycall.rb
|
|
48
|
+
- lib/straycall/cli.rb
|
|
49
|
+
- lib/straycall/config.rb
|
|
50
|
+
- lib/straycall/handlers/exec.rb
|
|
51
|
+
- lib/straycall/handlers/filesystem.rb
|
|
52
|
+
- lib/straycall/in_process.rb
|
|
53
|
+
- lib/straycall/minitest.rb
|
|
54
|
+
- lib/straycall/policy.rb
|
|
55
|
+
- lib/straycall/policy/exec.rb
|
|
56
|
+
- lib/straycall/policy/filesystem.rb
|
|
57
|
+
- lib/straycall/policy/network.rb
|
|
58
|
+
- lib/straycall/prompt.rb
|
|
59
|
+
- lib/straycall/recorder.rb
|
|
60
|
+
- lib/straycall/report/console.rb
|
|
61
|
+
- lib/straycall/report/json.rb
|
|
62
|
+
- lib/straycall/reporter/client.rb
|
|
63
|
+
- lib/straycall/reporter/protocol.rb
|
|
64
|
+
- lib/straycall/reporter/symbolizer.rb
|
|
65
|
+
- lib/straycall/reporter/thread_reporter.rb
|
|
66
|
+
- lib/straycall/rspec.rb
|
|
67
|
+
- lib/straycall/supervisor.rb
|
|
68
|
+
- lib/straycall/target.rb
|
|
69
|
+
- lib/straycall/version.rb
|
|
70
|
+
- lib/straycall/violation.rb
|
|
71
|
+
homepage: https://rubygems.org/gems/straycall
|
|
72
|
+
licenses:
|
|
73
|
+
- MIT
|
|
74
|
+
metadata:
|
|
75
|
+
rubygems_mfa_required: 'true'
|
|
76
|
+
homepage_uri: https://rubygems.org/gems/straycall
|
|
77
|
+
rdoc_options: []
|
|
78
|
+
require_paths:
|
|
79
|
+
- lib
|
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: 3.1.0
|
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
requirements: []
|
|
91
|
+
rubygems_version: 4.0.19
|
|
92
|
+
specification_version: 4
|
|
93
|
+
summary: Report and block unintended syscalls in Ruby builds and tests
|
|
94
|
+
test_files: []
|