endpoint_security 0.1.0-universal-darwin
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/.rubocop.yml +44 -0
- data/LICENSE.txt +21 -0
- data/README.md +114 -0
- data/Rakefile +170 -0
- data/codegen/ast.rb +67 -0
- data/codegen/emit_c.rb +68 -0
- data/codegen/emit_ruby.rb +128 -0
- data/codegen/ir.rb +193 -0
- data/codegen/overlay/availability.yml +158 -0
- data/codegen/overlay/version_map.yml +92 -0
- data/codegen/run.rb +80 -0
- data/codegen/snapshots/26.5.json +5828 -0
- data/examples/eslogger_clone.rb +5 -0
- data/examples/execblock.rb +16 -0
- data/examples/filemon.rb +10 -0
- data/examples/procmon.rb +13 -0
- data/ext/endpoint_security/client.c +795 -0
- data/ext/endpoint_security/client.h +88 -0
- data/ext/endpoint_security/endpoint_security.c +17 -0
- data/ext/endpoint_security/extconf.rb +37 -0
- data/ext/endpoint_security/field.c +719 -0
- data/ext/endpoint_security/field.h +42 -0
- data/ext/endpoint_security/generated/es_schema.c +1233 -0
- data/ext/endpoint_security/message.c +426 -0
- data/ext/endpoint_security/message.h +13 -0
- data/ext/endpoint_security/mute.c +291 -0
- data/ext/endpoint_security/mute.h +8 -0
- data/ext/endpoint_security/queue.c +124 -0
- data/ext/endpoint_security/queue.h +46 -0
- data/ext/endpoint_security/watchdog.c +243 -0
- data/lib/endpoint_security/availability.rb +30 -0
- data/lib/endpoint_security/client.rb +280 -0
- data/lib/endpoint_security/diagnostics.rb +26 -0
- data/lib/endpoint_security/doctor.rb +35 -0
- data/lib/endpoint_security/errors.rb +42 -0
- data/lib/endpoint_security/generated/availability.rb +169 -0
- data/lib/endpoint_security/generated/enums.rb +409 -0
- data/lib/endpoint_security/generated/event_types.rb +520 -0
- data/lib/endpoint_security/message.rb +29 -0
- data/lib/endpoint_security/object_model.rb +170 -0
- data/lib/endpoint_security/recorder.rb +26 -0
- data/lib/endpoint_security/version.rb +6 -0
- data/lib/endpoint_security.rb +38 -0
- data/support/entitlements.plist +8 -0
- data/support/esmock/esmock.c +482 -0
- data/support/esmock/esmock.h +21 -0
- data/support/sign_ruby.sh +13 -0
- metadata +90 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/* watchdog.c
|
|
2
|
+
* Calling threads: arm = ES handler without the GVL; heap owner = dedicated pthread without the GVL.
|
|
3
|
+
* The handler only publishes to a bounded lock-free queue. Ruby APIs are forbidden.
|
|
4
|
+
*/
|
|
5
|
+
#include "client.h"
|
|
6
|
+
|
|
7
|
+
#include <mach/mach_time.h>
|
|
8
|
+
#include <stdlib.h>
|
|
9
|
+
#include <time.h>
|
|
10
|
+
|
|
11
|
+
static void
|
|
12
|
+
heap_swap(esrb_watchdog_t *watchdog, size_t left, size_t right)
|
|
13
|
+
{
|
|
14
|
+
esrb_watchdog_entry_t temporary = watchdog->heap[left];
|
|
15
|
+
watchdog->heap[left] = watchdog->heap[right];
|
|
16
|
+
watchdog->heap[right] = temporary;
|
|
17
|
+
watchdog->heap[left].slot->watchdog_index = left;
|
|
18
|
+
watchdog->heap[right].slot->watchdog_index = right;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static void
|
|
22
|
+
heap_up(esrb_watchdog_t *watchdog, size_t index)
|
|
23
|
+
{
|
|
24
|
+
while (index > 0) {
|
|
25
|
+
size_t parent = (index - 1) / 2;
|
|
26
|
+
if (watchdog->heap[parent].fire_at <= watchdog->heap[index].fire_at) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
heap_swap(watchdog, parent, index);
|
|
30
|
+
index = parent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static void
|
|
35
|
+
heap_down(esrb_watchdog_t *watchdog, size_t index)
|
|
36
|
+
{
|
|
37
|
+
for (;;) {
|
|
38
|
+
size_t left = index * 2 + 1;
|
|
39
|
+
if (left >= watchdog->heap_size) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
size_t right = left + 1;
|
|
43
|
+
size_t smallest = right < watchdog->heap_size && watchdog->heap[right].fire_at < watchdog->heap[left].fire_at
|
|
44
|
+
? right
|
|
45
|
+
: left;
|
|
46
|
+
if (watchdog->heap[index].fire_at <= watchdog->heap[smallest].fire_at) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
heap_swap(watchdog, index, smallest);
|
|
50
|
+
index = smallest;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static void
|
|
55
|
+
heap_remove(esrb_watchdog_t *watchdog, size_t index)
|
|
56
|
+
{
|
|
57
|
+
esrb_slot_t *removed = watchdog->heap[index].slot;
|
|
58
|
+
removed->watchdog_index = SIZE_MAX;
|
|
59
|
+
watchdog->heap_size--;
|
|
60
|
+
if (index == watchdog->heap_size) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
watchdog->heap[index] = watchdog->heap[watchdog->heap_size];
|
|
64
|
+
watchdog->heap[index].slot->watchdog_index = index;
|
|
65
|
+
esrb_slot_t *moved = watchdog->heap[index].slot;
|
|
66
|
+
heap_up(watchdog, index);
|
|
67
|
+
heap_down(watchdog, moved->watchdog_index);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static void
|
|
71
|
+
heap_set(esrb_watchdog_t *watchdog, const esrb_watchdog_request_t *request)
|
|
72
|
+
{
|
|
73
|
+
size_t index = request->slot->watchdog_index;
|
|
74
|
+
if (index >= watchdog->heap_size || watchdog->heap[index].slot != request->slot) {
|
|
75
|
+
if (watchdog->heap_size >= watchdog->capacity) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
index = watchdog->heap_size++;
|
|
79
|
+
request->slot->watchdog_index = index;
|
|
80
|
+
}
|
|
81
|
+
watchdog->heap[index] = (esrb_watchdog_entry_t){
|
|
82
|
+
.slot = request->slot, .position = request->position, .fire_at = request->fire_at
|
|
83
|
+
};
|
|
84
|
+
heap_up(watchdog, index);
|
|
85
|
+
heap_down(watchdog, request->slot->watchdog_index);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static bool
|
|
89
|
+
request_pop(esrb_watchdog_t *watchdog, esrb_watchdog_request_t *output)
|
|
90
|
+
{
|
|
91
|
+
size_t position = atomic_load_explicit(&watchdog->dequeue_position, memory_order_relaxed);
|
|
92
|
+
esrb_watchdog_request_t *request = &watchdog->requests[position & watchdog->mask];
|
|
93
|
+
if (atomic_load_explicit(&request->sequence, memory_order_acquire) != position + 1) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
atomic_store_explicit(&watchdog->dequeue_position, position + 1, memory_order_relaxed);
|
|
97
|
+
output->slot = request->slot;
|
|
98
|
+
output->position = request->position;
|
|
99
|
+
output->fire_at = request->fire_at;
|
|
100
|
+
atomic_store_explicit(&request->sequence, position + watchdog->capacity, memory_order_release);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
bool
|
|
105
|
+
esrb_watchdog_init(esrb_watchdog_t *watchdog, size_t capacity)
|
|
106
|
+
{
|
|
107
|
+
watchdog->capacity = capacity;
|
|
108
|
+
watchdog->mask = capacity - 1;
|
|
109
|
+
watchdog->heap_size = 0;
|
|
110
|
+
watchdog->requests = calloc(capacity, sizeof(*watchdog->requests));
|
|
111
|
+
watchdog->heap = calloc(capacity, sizeof(*watchdog->heap));
|
|
112
|
+
if (watchdog->requests == NULL || watchdog->heap == NULL) {
|
|
113
|
+
free(watchdog->requests);
|
|
114
|
+
free(watchdog->heap);
|
|
115
|
+
watchdog->requests = NULL;
|
|
116
|
+
watchdog->heap = NULL;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
atomic_init(&watchdog->enqueue_position, 0);
|
|
120
|
+
atomic_init(&watchdog->dequeue_position, 0);
|
|
121
|
+
for (size_t index = 0; index < capacity; index++) {
|
|
122
|
+
atomic_init(&watchdog->requests[index].sequence, index);
|
|
123
|
+
}
|
|
124
|
+
if (pthread_mutex_init(&watchdog->mutex, NULL) != 0) {
|
|
125
|
+
free(watchdog->requests);
|
|
126
|
+
free(watchdog->heap);
|
|
127
|
+
watchdog->requests = NULL;
|
|
128
|
+
watchdog->heap = NULL;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (pthread_cond_init(&watchdog->condition, NULL) != 0) {
|
|
132
|
+
pthread_mutex_destroy(&watchdog->mutex);
|
|
133
|
+
free(watchdog->requests);
|
|
134
|
+
free(watchdog->heap);
|
|
135
|
+
watchdog->requests = NULL;
|
|
136
|
+
watchdog->heap = NULL;
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
void
|
|
143
|
+
esrb_watchdog_destroy(esrb_watchdog_t *watchdog)
|
|
144
|
+
{
|
|
145
|
+
if (watchdog->requests == NULL) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
pthread_cond_destroy(&watchdog->condition);
|
|
149
|
+
pthread_mutex_destroy(&watchdog->mutex);
|
|
150
|
+
free(watchdog->requests);
|
|
151
|
+
free(watchdog->heap);
|
|
152
|
+
watchdog->requests = NULL;
|
|
153
|
+
watchdog->heap = NULL;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
void
|
|
157
|
+
esrb_watchdog_abandon(esrb_watchdog_t *watchdog)
|
|
158
|
+
{
|
|
159
|
+
free(watchdog->requests);
|
|
160
|
+
free(watchdog->heap);
|
|
161
|
+
watchdog->requests = NULL;
|
|
162
|
+
watchdog->heap = NULL;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
bool
|
|
166
|
+
esrb_watchdog_arm(esrb_watchdog_t *watchdog, esrb_slot_t *slot)
|
|
167
|
+
{
|
|
168
|
+
size_t position = atomic_load_explicit(&watchdog->enqueue_position, memory_order_relaxed);
|
|
169
|
+
for (;;) {
|
|
170
|
+
esrb_watchdog_request_t *request = &watchdog->requests[position & watchdog->mask];
|
|
171
|
+
size_t sequence = atomic_load_explicit(&request->sequence, memory_order_acquire);
|
|
172
|
+
intptr_t difference = (intptr_t)sequence - (intptr_t)position;
|
|
173
|
+
if (difference == 0) {
|
|
174
|
+
if (atomic_compare_exchange_weak_explicit(&watchdog->enqueue_position, &position, position + 1,
|
|
175
|
+
memory_order_relaxed, memory_order_relaxed)) {
|
|
176
|
+
request->slot = slot;
|
|
177
|
+
request->position = slot->position;
|
|
178
|
+
request->fire_at = slot->fire_at;
|
|
179
|
+
atomic_store_explicit(&request->sequence, position + 1, memory_order_release);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
} else if (difference < 0) {
|
|
183
|
+
return false;
|
|
184
|
+
} else {
|
|
185
|
+
position = atomic_load_explicit(&watchdog->enqueue_position, memory_order_relaxed);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
static void
|
|
191
|
+
answer_due(esrb_client_t *client, esrb_watchdog_entry_t entry)
|
|
192
|
+
{
|
|
193
|
+
esrb_slot_t *slot = entry.slot;
|
|
194
|
+
if (!atomic_load_explicit(&slot->occupied, memory_order_acquire)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
atomic_fetch_add_explicit(&slot->readers, 1, memory_order_acquire);
|
|
198
|
+
if (!atomic_load_explicit(&slot->occupied, memory_order_acquire) || slot->position != entry.position) {
|
|
199
|
+
atomic_fetch_sub_explicit(&slot->readers, 1, memory_order_release);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
uint32_t expected = ESRB_ANSWER_PENDING;
|
|
203
|
+
if (atomic_compare_exchange_strong_explicit(
|
|
204
|
+
&slot->answer_state, &expected, ESRB_ANSWER_ANSWERED, memory_order_acq_rel, memory_order_acquire)) {
|
|
205
|
+
esrb_send_response(client, slot->client, slot->message, client->default_auth,
|
|
206
|
+
client->default_auth == ES_AUTH_RESULT_ALLOW ? UINT32_MAX : 0, client->default_cache);
|
|
207
|
+
atomic_fetch_add_explicit(&client->timeouts, 1, memory_order_relaxed);
|
|
208
|
+
esrb_notify(client);
|
|
209
|
+
}
|
|
210
|
+
atomic_fetch_sub_explicit(&slot->readers, 1, memory_order_release);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
void *
|
|
214
|
+
esrb_watchdog_main(void *argument)
|
|
215
|
+
{
|
|
216
|
+
esrb_client_t *client = argument;
|
|
217
|
+
esrb_watchdog_t *watchdog = &client->watchdog;
|
|
218
|
+
while (atomic_load_explicit(&client->watchdog_running, memory_order_acquire)) {
|
|
219
|
+
esrb_watchdog_request_t request;
|
|
220
|
+
while (request_pop(watchdog, &request)) {
|
|
221
|
+
heap_set(watchdog, &request);
|
|
222
|
+
}
|
|
223
|
+
uint64_t now = mach_absolute_time();
|
|
224
|
+
while (watchdog->heap_size > 0 && watchdog->heap[0].fire_at <= now) {
|
|
225
|
+
esrb_watchdog_entry_t entry = watchdog->heap[0];
|
|
226
|
+
heap_remove(watchdog, 0);
|
|
227
|
+
answer_due(client, entry);
|
|
228
|
+
now = mach_absolute_time();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
struct timespec wake;
|
|
232
|
+
clock_gettime(CLOCK_REALTIME, &wake);
|
|
233
|
+
wake.tv_nsec += 1000000;
|
|
234
|
+
if (wake.tv_nsec >= 1000000000) {
|
|
235
|
+
wake.tv_sec++;
|
|
236
|
+
wake.tv_nsec -= 1000000000;
|
|
237
|
+
}
|
|
238
|
+
pthread_mutex_lock(&watchdog->mutex);
|
|
239
|
+
pthread_cond_timedwait(&watchdog->condition, &watchdog->mutex, &wake);
|
|
240
|
+
pthread_mutex_unlock(&watchdog->mutex);
|
|
241
|
+
}
|
|
242
|
+
return NULL;
|
|
243
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module EndpointSecurity
|
|
6
|
+
# Checks whether events exist on the running macOS version.
|
|
7
|
+
module Availability
|
|
8
|
+
@probe_cache = {}
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# @return [Gem::Version] running macOS version
|
|
13
|
+
def runtime_version
|
|
14
|
+
@runtime_version ||= Gem::Version.new(Open3.capture2("sw_vers", "-productVersion").first.strip)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [Boolean] whether +event+ is available on this macOS version
|
|
18
|
+
def supported_event?(event)
|
|
19
|
+
runtime_version >= Gem::Version.new(EVENT_MIN_OS.fetch(event.to_sym))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Results of native one-event subscription probes.
|
|
23
|
+
# @return [Hash<Symbol, Boolean>]
|
|
24
|
+
def probe_cache = @probe_cache
|
|
25
|
+
|
|
26
|
+
# Clears native subscription probe results.
|
|
27
|
+
# @return [Hash] emptied cache
|
|
28
|
+
def clear_probe_cache! = @probe_cache.clear
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/wait"
|
|
4
|
+
|
|
5
|
+
module EndpointSecurity
|
|
6
|
+
# Owns an Endpoint Security client and dispatches subscribed messages.
|
|
7
|
+
class Client
|
|
8
|
+
class << self
|
|
9
|
+
# @yield [client]
|
|
10
|
+
# @return [Object]
|
|
11
|
+
def open(**)
|
|
12
|
+
client = new(**)
|
|
13
|
+
return client unless block_given?
|
|
14
|
+
|
|
15
|
+
yield client
|
|
16
|
+
ensure
|
|
17
|
+
client&.close if block_given?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Calls the native constructor.
|
|
22
|
+
# @api private
|
|
23
|
+
alias __native_initialize initialize
|
|
24
|
+
# Calls the native close implementation.
|
|
25
|
+
# @api private
|
|
26
|
+
alias __native_close close
|
|
27
|
+
# Calls the native statistics implementation.
|
|
28
|
+
# @api private
|
|
29
|
+
alias __native_stats stats
|
|
30
|
+
|
|
31
|
+
# @return [Client]
|
|
32
|
+
def initialize(mute_self: true, subscribe: nil, probe: :lazy, **)
|
|
33
|
+
raise ArgumentError, "probe must be :lazy, :eager, or :off" unless %i[lazy eager off].include?(probe.to_sym)
|
|
34
|
+
|
|
35
|
+
__native_initialize(mute_self: mute_self, **)
|
|
36
|
+
initialized = false
|
|
37
|
+
begin
|
|
38
|
+
@probe = probe.to_sym
|
|
39
|
+
@handlers = {}
|
|
40
|
+
@subscriptions = []
|
|
41
|
+
@errors = 0
|
|
42
|
+
@reported_timeouts = 0
|
|
43
|
+
@running = false
|
|
44
|
+
mute_process(pid: ::Process.pid) if mute_self
|
|
45
|
+
if @probe == :eager
|
|
46
|
+
EventType.all.each do |event|
|
|
47
|
+
Availability.probe_cache[event] = probe_event(event) if Availability.supported_event?(event)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
self.subscribe(subscribe) if subscribe
|
|
51
|
+
initialized = true
|
|
52
|
+
ensure
|
|
53
|
+
__native_close unless initialized
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Array<Symbol>]
|
|
58
|
+
def subscribe(*events, skip_unsupported: true, **_options)
|
|
59
|
+
events = events.flatten.map(&:to_sym) - @subscriptions
|
|
60
|
+
unsupported = events.reject { |event| supported_event?(event) }
|
|
61
|
+
if !skip_unsupported && !unsupported.empty?
|
|
62
|
+
raise UnsupportedEventError, "unsupported events: #{unsupported.join(", ")}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
events -= unsupported
|
|
66
|
+
return @subscriptions if events.empty?
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
__subscribe(events.map { |event| EventType.value(event) })
|
|
70
|
+
rescue SubscriptionError => e
|
|
71
|
+
failed = events.reject { |event| probe_event(event) }
|
|
72
|
+
failed = events if failed.empty?
|
|
73
|
+
raise SubscriptionError, "failed to subscribe: #{failed.join(", ")} (#{e.message})"
|
|
74
|
+
end
|
|
75
|
+
@subscriptions |= events
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Array<Symbol>] remaining subscriptions
|
|
79
|
+
def unsubscribe(*events)
|
|
80
|
+
events = events.flatten.map(&:to_sym)
|
|
81
|
+
return @subscriptions if events.empty?
|
|
82
|
+
|
|
83
|
+
__unsubscribe(events.map { |event| EventType.value(event) })
|
|
84
|
+
@subscriptions -= events
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Array] empty subscription list
|
|
88
|
+
def unsubscribe_all
|
|
89
|
+
__unsubscribe(nil)
|
|
90
|
+
@subscriptions.clear
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Array<Symbol>] subscriptions reported by Endpoint Security
|
|
94
|
+
def subscriptions = __subscriptions
|
|
95
|
+
|
|
96
|
+
# @return [Client]
|
|
97
|
+
def on(event, &handler)
|
|
98
|
+
raise ArgumentError, "handler block is required" unless handler
|
|
99
|
+
|
|
100
|
+
@handlers[event.to_sym] = handler
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [Client]
|
|
105
|
+
def on_error(&handler)
|
|
106
|
+
@error_handler = handler
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Client]
|
|
111
|
+
def on_timeout(&handler)
|
|
112
|
+
@timeout_handler = handler
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Mutes other Endpoint Security clients when their first event is observed.
|
|
117
|
+
# @return [Client]
|
|
118
|
+
def mute_all_es_clients!
|
|
119
|
+
@mute_es_clients = true
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @return [Client]
|
|
124
|
+
def run
|
|
125
|
+
@running = true
|
|
126
|
+
wakeup = IO.for_fd(__wakeup_fd, autoclose: false)
|
|
127
|
+
while @running && !closed?
|
|
128
|
+
wakeup.wait_readable(0.1)
|
|
129
|
+
__drain.each { |message| dispatch(message) }
|
|
130
|
+
report_timeouts
|
|
131
|
+
end
|
|
132
|
+
self
|
|
133
|
+
ensure
|
|
134
|
+
@running = false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @return [Thread]
|
|
138
|
+
def start
|
|
139
|
+
return @thread if @thread&.alive?
|
|
140
|
+
|
|
141
|
+
@thread = ::Thread.new { run }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @return [Client]
|
|
145
|
+
def stop
|
|
146
|
+
@running = false
|
|
147
|
+
__wake unless closed?
|
|
148
|
+
@thread&.join unless @thread == ::Thread.current
|
|
149
|
+
self
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @return [nil]
|
|
153
|
+
def close
|
|
154
|
+
if @thread == ::Thread.current && @dispatching
|
|
155
|
+
@running = false
|
|
156
|
+
@close_after_dispatch = true
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
stop
|
|
161
|
+
__native_close
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# @return [Hash]
|
|
165
|
+
def stats
|
|
166
|
+
__native_stats.merge(errors: @errors)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Native values for path mute kinds.
|
|
170
|
+
# @api private
|
|
171
|
+
PATH_TYPES = { prefix: 0, literal: 1, target_prefix: 2, target_literal: 3 }.freeze
|
|
172
|
+
# Native values for mute inversion kinds.
|
|
173
|
+
# @api private
|
|
174
|
+
INVERSION_TYPES = { process: 0, path: 1, target_path: 2 }.freeze
|
|
175
|
+
|
|
176
|
+
# Mutes events for +path+.
|
|
177
|
+
def mute_path(path, type: :prefix) = change_path_mute(:mute, path, type, [])
|
|
178
|
+
# Removes a path mute.
|
|
179
|
+
def unmute_path(path, type: :prefix) = change_path_mute(:unmute, path, type, [])
|
|
180
|
+
# Mutes selected +events+ for +path+.
|
|
181
|
+
def mute_path_events(path, *events, type: :prefix) = change_path_mute(:mute, path, type, events)
|
|
182
|
+
# Removes selected event mutes for +path+.
|
|
183
|
+
def unmute_path_events(path, *events, type: :prefix) = change_path_mute(:unmute, path, type, events)
|
|
184
|
+
|
|
185
|
+
# Mutes a process by audit token or PID.
|
|
186
|
+
def mute_process(token = nil, pid: nil)
|
|
187
|
+
change_process_mute(:mute, token || (__audit_token_for_pid(pid) if pid), [])
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Removes a process mute by audit token or PID.
|
|
191
|
+
def unmute_process(token = nil, pid: nil)
|
|
192
|
+
change_process_mute(:unmute, token || (__audit_token_for_pid(pid) if pid), [])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Mutes selected +events+ for a process.
|
|
196
|
+
def mute_process_events(token, *events) = change_process_mute(:mute, token, events)
|
|
197
|
+
# Removes selected event mutes for a process.
|
|
198
|
+
def unmute_process_events(token, *events) = change_process_mute(:unmute, token, events)
|
|
199
|
+
# Removes every source-path mute.
|
|
200
|
+
def unmute_all_paths = __unmute_all_paths(false)
|
|
201
|
+
# Removes every target-path mute.
|
|
202
|
+
def unmute_all_target_paths = __unmute_all_paths(true)
|
|
203
|
+
# Clears the Endpoint Security authorization cache.
|
|
204
|
+
def clear_cache = __clear_cache
|
|
205
|
+
|
|
206
|
+
# Inverts muting for +type+.
|
|
207
|
+
def invert_muting(type)
|
|
208
|
+
__invert_muting(INVERSION_TYPES.fetch(type.to_sym))
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# @return [Boolean] whether muting for +type+ is inverted
|
|
212
|
+
def muting_inverted?(type)
|
|
213
|
+
__muting_inverted(INVERSION_TYPES.fetch(type.to_sym))
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# @return [Array<Hash>] copied path mute entries
|
|
217
|
+
def muted_paths
|
|
218
|
+
__muted_paths.map { |item| item.merge(type: PATH_TYPES.key(item[:type]) || item[:type]) }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# @return [Array<Hash>] copied process mute entries
|
|
222
|
+
def muted_processes = __muted_processes
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def supported_event?(event)
|
|
227
|
+
return false unless Availability.supported_event?(event)
|
|
228
|
+
return true if @probe == :off
|
|
229
|
+
|
|
230
|
+
Availability.probe_cache.fetch(event) { Availability.probe_cache[event] = probe_event(event) }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def probe_event(event)
|
|
234
|
+
value = EventType.value(event)
|
|
235
|
+
__subscribe([value])
|
|
236
|
+
__unsubscribe([value])
|
|
237
|
+
true
|
|
238
|
+
rescue SubscriptionError
|
|
239
|
+
false
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def change_path_mute(action, path, type, events)
|
|
243
|
+
__mute_path(action, String(path), PATH_TYPES.fetch(type.to_sym), events.map { |event| EventType.value(event) })
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def change_process_mute(action, token, events)
|
|
247
|
+
raise ArgumentError, "audit token or pid is required" unless token
|
|
248
|
+
|
|
249
|
+
__mute_process(action, token, events.map { |event| EventType.value(event) })
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def dispatch(message)
|
|
253
|
+
@dispatching = true
|
|
254
|
+
if @mute_es_clients && message.process.es_client?
|
|
255
|
+
mute_process(message.process.audit_token)
|
|
256
|
+
return
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
handler = @handlers[message.event_type]
|
|
260
|
+
handler&.call(message)
|
|
261
|
+
rescue StandardError => e
|
|
262
|
+
@errors += 1
|
|
263
|
+
message.__respond_default! if message.auth? && !message.answered?
|
|
264
|
+
@error_handler&.call(e)
|
|
265
|
+
ensure
|
|
266
|
+
message.__auto_release!
|
|
267
|
+
@dispatching = false
|
|
268
|
+
if @close_after_dispatch
|
|
269
|
+
@close_after_dispatch = false
|
|
270
|
+
__native_close
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def report_timeouts
|
|
275
|
+
count = __native_stats[:timeouts]
|
|
276
|
+
(count - @reported_timeouts).times { @timeout_handler&.call }
|
|
277
|
+
@reported_timeouts = count
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EndpointSecurity
|
|
4
|
+
# Turns native client creation results into actionable messages.
|
|
5
|
+
module Diagnostics
|
|
6
|
+
# Human-readable explanations keyed by native result.
|
|
7
|
+
EXPLANATIONS = {
|
|
8
|
+
success: "Endpoint Security client created successfully.",
|
|
9
|
+
err_invalid_argument: "Endpoint Security rejected an invalid argument; this is likely a binding bug.",
|
|
10
|
+
err_internal: "Endpoint Security reported an internal error; retry after checking system logs.",
|
|
11
|
+
err_not_entitled: "The host Ruby executable lacks the Endpoint Security client entitlement.",
|
|
12
|
+
err_not_permitted: "Grant Full Disk Access to the host Ruby executable.",
|
|
13
|
+
err_not_privileged: "Run the signed host Ruby executable as root.",
|
|
14
|
+
err_too_many_clients: "Close another Endpoint Security client and retry."
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# @return [String] explanation for +result+
|
|
20
|
+
def explain(result)
|
|
21
|
+
EXPLANATIONS.fetch(result.respond_to?(:to_sym) ? result.to_sym : result) do
|
|
22
|
+
"Unknown es_new_client result: #{result.inspect}."
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module EndpointSecurity
|
|
6
|
+
# Reports whether a development host meets Endpoint Security requirements.
|
|
7
|
+
module Doctor
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Prints development host checks to +out+.
|
|
11
|
+
# @return [void]
|
|
12
|
+
def run(out: $stdout)
|
|
13
|
+
out.puts "Endpoint Security development host diagnosis"
|
|
14
|
+
check(out, "macOS 13+", Gem::Version.new(`sw_vers -productVersion`.strip) >= Gem::Version.new("13"))
|
|
15
|
+
check(out, "root", Process.euid.zero?)
|
|
16
|
+
check(out, "SIP disabled", command("csrutil", "status").include?("disabled"))
|
|
17
|
+
check(out, "AMFI development boot arg", command("nvram", "boot-args").include?("amfi_get_out_of_my_way=0x1"))
|
|
18
|
+
out.puts "WARNING: Disable SIP only on a dedicated development machine."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @api private
|
|
22
|
+
# @return [String] captured command output
|
|
23
|
+
def command(*argv)
|
|
24
|
+
Open3.capture2e(*argv).first
|
|
25
|
+
rescue Errno::ENOENT
|
|
26
|
+
""
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @api private
|
|
30
|
+
# @return [void]
|
|
31
|
+
def check(out, label, available)
|
|
32
|
+
out.puts format("%<label>-28s %<status>s", label: label, status: available ? "OK" : "MISSING")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EndpointSecurity
|
|
4
|
+
# Base error for this library.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
# Endpoint Security client creation failed.
|
|
7
|
+
class ClientError < Error; end
|
|
8
|
+
# The host executable lacks the Endpoint Security entitlement.
|
|
9
|
+
class NotEntitledError < ClientError; end
|
|
10
|
+
# The host executable lacks Full Disk Access.
|
|
11
|
+
class NotPermittedError < ClientError; end
|
|
12
|
+
# The host process is not privileged.
|
|
13
|
+
class NotPrivilegedError < ClientError; end
|
|
14
|
+
# The system has reached its Endpoint Security client limit.
|
|
15
|
+
class TooManyClientsError < ClientError; end
|
|
16
|
+
# Endpoint Security rejected an invalid argument.
|
|
17
|
+
class InvalidArgumentError < ClientError; end
|
|
18
|
+
# Endpoint Security reported an internal error.
|
|
19
|
+
class InternalError < ClientError; end
|
|
20
|
+
# An event subscription operation failed.
|
|
21
|
+
class SubscriptionError < Error; end
|
|
22
|
+
# An event is unavailable on the running macOS version.
|
|
23
|
+
class UnsupportedEventError < SubscriptionError; end
|
|
24
|
+
# A mute operation failed.
|
|
25
|
+
class MuteError < Error; end
|
|
26
|
+
# Base error for message operations.
|
|
27
|
+
class MessageError < Error; end
|
|
28
|
+
# A lazy message view outlived its native message.
|
|
29
|
+
class MessageInvalidatedError < MessageError; end
|
|
30
|
+
# An authorization message has already been answered.
|
|
31
|
+
class AlreadyAnsweredError < MessageError; end
|
|
32
|
+
# A field is absent from this message version.
|
|
33
|
+
class FieldUnavailableError < MessageError; end
|
|
34
|
+
# Caching was requested for a non-cacheable event.
|
|
35
|
+
class NonCacheableEventError < MessageError; end
|
|
36
|
+
# The running macOS version lacks a requested native API.
|
|
37
|
+
class UnsupportedAPIError < Error; end
|
|
38
|
+
# A client created before fork was used by the child process.
|
|
39
|
+
class ForkedClientError < Error; end
|
|
40
|
+
# SDK metadata generation failed.
|
|
41
|
+
class CodegenError < Error; end
|
|
42
|
+
end
|