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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +44 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +114 -0
  5. data/Rakefile +170 -0
  6. data/codegen/ast.rb +67 -0
  7. data/codegen/emit_c.rb +68 -0
  8. data/codegen/emit_ruby.rb +128 -0
  9. data/codegen/ir.rb +193 -0
  10. data/codegen/overlay/availability.yml +158 -0
  11. data/codegen/overlay/version_map.yml +92 -0
  12. data/codegen/run.rb +80 -0
  13. data/codegen/snapshots/26.5.json +5828 -0
  14. data/examples/eslogger_clone.rb +5 -0
  15. data/examples/execblock.rb +16 -0
  16. data/examples/filemon.rb +10 -0
  17. data/examples/procmon.rb +13 -0
  18. data/ext/endpoint_security/client.c +795 -0
  19. data/ext/endpoint_security/client.h +88 -0
  20. data/ext/endpoint_security/endpoint_security.c +17 -0
  21. data/ext/endpoint_security/extconf.rb +37 -0
  22. data/ext/endpoint_security/field.c +719 -0
  23. data/ext/endpoint_security/field.h +42 -0
  24. data/ext/endpoint_security/generated/es_schema.c +1233 -0
  25. data/ext/endpoint_security/message.c +426 -0
  26. data/ext/endpoint_security/message.h +13 -0
  27. data/ext/endpoint_security/mute.c +291 -0
  28. data/ext/endpoint_security/mute.h +8 -0
  29. data/ext/endpoint_security/queue.c +124 -0
  30. data/ext/endpoint_security/queue.h +46 -0
  31. data/ext/endpoint_security/watchdog.c +243 -0
  32. data/lib/endpoint_security/availability.rb +30 -0
  33. data/lib/endpoint_security/client.rb +280 -0
  34. data/lib/endpoint_security/diagnostics.rb +26 -0
  35. data/lib/endpoint_security/doctor.rb +35 -0
  36. data/lib/endpoint_security/errors.rb +42 -0
  37. data/lib/endpoint_security/generated/availability.rb +169 -0
  38. data/lib/endpoint_security/generated/enums.rb +409 -0
  39. data/lib/endpoint_security/generated/event_types.rb +520 -0
  40. data/lib/endpoint_security/message.rb +29 -0
  41. data/lib/endpoint_security/object_model.rb +170 -0
  42. data/lib/endpoint_security/recorder.rb +26 -0
  43. data/lib/endpoint_security/version.rb +6 -0
  44. data/lib/endpoint_security.rb +38 -0
  45. data/support/entitlements.plist +8 -0
  46. data/support/esmock/esmock.c +482 -0
  47. data/support/esmock/esmock.h +21 -0
  48. data/support/sign_ruby.sh +13 -0
  49. 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