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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4b0bc527d2dfed0f2977454734657cbaf7135a4ed15cff6669df95309c422d23
|
|
4
|
+
data.tar.gz: 22b8aa3d46cfa618dc86d05be8ad9b69d69bdca6926f40c87d4b3d4afb92fa4e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ec91461e3365f8d76cddbe0e76f0a857f8080acaa1f14165b6aac1d1e2d1986cc4b50b2ca7c4ab9a4d6c4eaa4f401093f646dbfaa3e008a7dd7b338b1dcf7c5b
|
|
7
|
+
data.tar.gz: a764942a1debc27dbcb3d9c7d702b900a915febe9a7e50379b3df243f025962fd898734e46bfb3a5e20d34d7669487ea95a295d57996ca27d6af285d208b1b4b
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
CacheRootDirectory: tmp/rubocop_cache
|
|
3
|
+
NewCops: enable
|
|
4
|
+
TargetRubyVersion: 3.2
|
|
5
|
+
Exclude:
|
|
6
|
+
- "tmp/**/*"
|
|
7
|
+
- "vendor/**/*"
|
|
8
|
+
- "lib/endpoint_security/generated/**/*"
|
|
9
|
+
- "ext/endpoint_security/generated/**/*"
|
|
10
|
+
|
|
11
|
+
Style/Documentation:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
Style/StringLiterals:
|
|
15
|
+
EnforcedStyle: double_quotes
|
|
16
|
+
|
|
17
|
+
Style/GlobalVars:
|
|
18
|
+
Exclude:
|
|
19
|
+
- "ext/**/extconf.rb"
|
|
20
|
+
|
|
21
|
+
Style/StringLiteralsInInterpolation:
|
|
22
|
+
Enabled: false
|
|
23
|
+
|
|
24
|
+
Naming/VariableNumber:
|
|
25
|
+
Enabled: false
|
|
26
|
+
|
|
27
|
+
Metrics/AbcSize:
|
|
28
|
+
Max: 45
|
|
29
|
+
|
|
30
|
+
Metrics/CyclomaticComplexity:
|
|
31
|
+
Max: 15
|
|
32
|
+
|
|
33
|
+
Metrics/PerceivedComplexity:
|
|
34
|
+
Max: 15
|
|
35
|
+
|
|
36
|
+
Metrics/MethodLength:
|
|
37
|
+
Max: 50
|
|
38
|
+
|
|
39
|
+
Metrics/ClassLength:
|
|
40
|
+
Max: 200
|
|
41
|
+
|
|
42
|
+
Metrics/BlockLength:
|
|
43
|
+
Exclude:
|
|
44
|
+
- "test/**/*"
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# endpoint_security
|
|
2
|
+
|
|
3
|
+
Native Ruby bindings for Apple's Endpoint Security API on macOS 13 or newer.
|
|
4
|
+
|
|
5
|
+
The gem keeps Apple's callback thread away from the Ruby VM: callbacks retain and enqueue messages into a bounded lock-free ring, a Ruby dispatcher invokes handlers, and a native watchdog guarantees one AUTH response before the deadline.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- macOS 13+
|
|
10
|
+
- CRuby 3.2+
|
|
11
|
+
- Xcode Command Line Tools
|
|
12
|
+
- A host Ruby executable signed with `com.apple.developer.endpoint-security.client`
|
|
13
|
+
- root privileges and Full Disk Access for that executable
|
|
14
|
+
|
|
15
|
+
The entitlement belongs to the host executable, not to this gem. Apple must grant it for production use.
|
|
16
|
+
|
|
17
|
+
For production, request `com.apple.developer.endpoint-security.client` for your Apple Developer team, sign a dedicated host executable with the approved entitlement, then grant that executable Full Disk Access. The helper under [Signing](#development) only performs the signing step; it cannot grant or bypass Apple's approval.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem "endpoint_security"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
bundle install
|
|
27
|
+
rake compile
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Observe events
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
require "endpoint_security"
|
|
34
|
+
|
|
35
|
+
ES::Client.open do |client|
|
|
36
|
+
client.subscribe(:notify_exec)
|
|
37
|
+
client.on(:notify_exec) do |message|
|
|
38
|
+
target = message.event.target
|
|
39
|
+
puts "#{target.executable.path} pid=#{target.pid}"
|
|
40
|
+
end
|
|
41
|
+
client.run
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Authorize events
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
ES::Client.open(auth_default: :allow, warn_on_truncated_path: true) do |client|
|
|
49
|
+
client.subscribe(:auth_exec)
|
|
50
|
+
client.on(:auth_exec) do |message|
|
|
51
|
+
target = message.event.target
|
|
52
|
+
target.platform_binary? ? message.allow!(cache: true) : message.deny!(cache: false)
|
|
53
|
+
end
|
|
54
|
+
client.run
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The native watchdog sends `auth_default` if Ruby misses the deadline. `allow!`, `deny!`, and the watchdog share one atomic state, so a message is never answered twice. Keep network, file, and subprocess I/O out of an AUTH handler; use `retain!` and another thread when slow work is unavoidable.
|
|
59
|
+
|
|
60
|
+
Do not authorize from a path string alone: paths can be truncated or replaced between observation and use. Prefer `cdhash`, `signing_id`, `team_id`, and platform-signing metadata; treat `path_truncated?` as unsafe for path-based decisions.
|
|
61
|
+
|
|
62
|
+
## Muting and recording
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
client.mute_path("/tmp/", type: :prefix)
|
|
66
|
+
client.mute_process(pid: Process.pid)
|
|
67
|
+
client.invert_muting(:path)
|
|
68
|
+
|
|
69
|
+
ES::Recorder.new(events: ES::EventType.all_notify, out: $stdout).run
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`Message#to_h` makes a JSON-compatible deep copy. Lazy `Event`, `Process`, and `File` views are invalid after the handler returns unless the message was retained.
|
|
73
|
+
|
|
74
|
+
## Development
|
|
75
|
+
|
|
76
|
+
Most work needs no entitlement:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
rake compile:mock
|
|
80
|
+
rake test
|
|
81
|
+
rake test:sanitize
|
|
82
|
+
rake test:drift
|
|
83
|
+
rake bench
|
|
84
|
+
rake rubocop
|
|
85
|
+
rake dev:doctor
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Generate metadata for a new SDK with `rake codegen`. Generated files under `ext/endpoint_security/generated` and `lib/endpoint_security/generated` must not be edited by hand.
|
|
89
|
+
|
|
90
|
+
For a dedicated development machine, create a signed Ruby copy:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
rake 'sign:ruby[/path/to/ruby,Developer ID Application: Example (TEAMID)]'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then grant `build/es-ruby` Full Disk Access and run the bounded integration test:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
RUN_ES_INTEGRATION=1 sudo -E ./build/es-ruby -S rake test:integration
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Do not disable SIP on a general-purpose machine. This project never changes SIP, NVRAM, TCC, or signing settings automatically.
|
|
103
|
+
|
|
104
|
+
## Limitations
|
|
105
|
+
|
|
106
|
+
- Fork after client creation is unsupported and raises `ES::ForkedClientError`.
|
|
107
|
+
- Undocumented `RESERVED_*` events expose their enum and `raw_event_bytes`, but no guessed structure.
|
|
108
|
+
- Real integration tests require Apple-granted entitlement, root, signing, and TCC; CI uses `libesmock`.
|
|
109
|
+
|
|
110
|
+
Public releases follow Semantic Versioning. Before 1.0, minor releases may contain breaking API changes; from 1.0 onward, breaking changes require a major release.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "rake/extensiontask"
|
|
7
|
+
require "rspec/core/rake_task"
|
|
8
|
+
|
|
9
|
+
Rake::ExtensionTask.new("endpoint_security") do |ext|
|
|
10
|
+
ext.ext_dir = "ext/endpoint_security"
|
|
11
|
+
ext.lib_dir = "lib/endpoint_security"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
RSpec::Core::RakeTask.new("test:unit") do |task|
|
|
15
|
+
task.pattern = "test/unit/**/*_test.rb"
|
|
16
|
+
task.rspec_opts = "-Itest"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
RSpec::Core::RakeTask.new("test:native:spec") do |task|
|
|
20
|
+
task.pattern = "test/native/**/*_test.rb"
|
|
21
|
+
task.rspec_opts = "-Itest"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
RSpec::Core::RakeTask.new("test:integration:spec") do |task|
|
|
25
|
+
task.pattern = "test/integration/**/*_test.rb"
|
|
26
|
+
task.rspec_opts = "-Itest"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
task "compile:mock" do
|
|
30
|
+
Rake::Task[:clobber].invoke
|
|
31
|
+
sdk = `xcrun --show-sdk-path`.strip
|
|
32
|
+
FileUtils.mkdir_p("build")
|
|
33
|
+
sh "xcrun", "clang", "-dynamiclib", "-fblocks", "-std=c11", "-Wall", "-Wextra",
|
|
34
|
+
"-Werror", "-mmacosx-version-min=13.0", "-isysroot", sdk, "-I#{sdk}/usr/include",
|
|
35
|
+
"-install_name", "@rpath/libesmock.dylib",
|
|
36
|
+
"support/esmock/esmock.c", "-o", "build/libesmock.dylib"
|
|
37
|
+
sh({ "ES_MOCK" => "1" }, RbConfig.ruby, "-S", "rake", "clobber", "compile")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
task "compile:real" do
|
|
41
|
+
sh({ "ES_MOCK" => nil }, RbConfig.ruby, "-S", "rake", "clobber", "compile")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
task "test:eslogger" do
|
|
45
|
+
require_relative "lib/endpoint_security/generated/event_types"
|
|
46
|
+
|
|
47
|
+
output, status = Open3.capture2e("/usr/bin/eslogger", "--list-events")
|
|
48
|
+
raise "eslogger --list-events failed: #{output}" unless status.success?
|
|
49
|
+
|
|
50
|
+
actual = output.lines.map { |line| line.strip.to_sym }.reject { |event| event.to_s.empty? }.sort
|
|
51
|
+
expected = EndpointSecurity::EventType.all_notify.map { |event| event.to_s.delete_prefix("notify_").to_sym }.sort
|
|
52
|
+
missing = expected - actual
|
|
53
|
+
extra = actual - expected
|
|
54
|
+
unless missing.empty? && extra.empty?
|
|
55
|
+
raise "eslogger drifted (missing: #{missing.join(", ")}; extra: #{extra.join(", ")})"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
task "test:api_surface" do
|
|
60
|
+
sdk = `xcrun --show-sdk-path`.strip
|
|
61
|
+
header = File.read(File.join(sdk, "usr/include/EndpointSecurity/ESClient.h")).gsub(%r{/\*.*?\*/}m, "")
|
|
62
|
+
declarations = header.split(";").filter_map do |statement|
|
|
63
|
+
name = statement.scan(/\b(es_[a-z0-9_]+)\s*\(/).last&.first
|
|
64
|
+
[name, statement.include?("API_DEPRECATED")] if name
|
|
65
|
+
end
|
|
66
|
+
native = Dir["ext/endpoint_security/*.c"].map { |path| File.read(path) }.join
|
|
67
|
+
native = native.gsub(%r{/\*.*?\*/}m, "").gsub(%r{//.*$}, "")
|
|
68
|
+
missing = declarations.reject(&:last).map(&:first).reject { |name| native.match?(/\b#{Regexp.escape(name)}\s*\(/) }
|
|
69
|
+
deprecated = declarations.select(&:last).map(&:first).select { |name| native.match?(/\b#{Regexp.escape(name)}\s*\(/) }
|
|
70
|
+
raise "unbound Endpoint Security functions: #{missing.join(", ")}" unless missing.empty?
|
|
71
|
+
raise "deprecated Endpoint Security functions used: #{deprecated.join(", ")}" unless deprecated.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# rubocop:disable-next Metrics/BlockLength
|
|
75
|
+
namespace :test do
|
|
76
|
+
task native: ["compile:mock", "test:native:spec"]
|
|
77
|
+
task :drift do
|
|
78
|
+
require_relative "lib/endpoint_security/generated/event_types"
|
|
79
|
+
|
|
80
|
+
generated_roots = "{codegen/overlay,codegen/snapshots," \
|
|
81
|
+
"ext/endpoint_security/generated,lib/endpoint_security/generated}"
|
|
82
|
+
files = Dir["#{generated_roots}/**/*"].select { |path| File.file?(path) }
|
|
83
|
+
before = files.to_h { |path| [path, File.binread(path)] }
|
|
84
|
+
Rake::Task[:codegen].invoke
|
|
85
|
+
after_files = Dir["#{generated_roots}/**/*"].select { |path| File.file?(path) }
|
|
86
|
+
changed = (files | after_files).reject { |path| before[path] == (File.binread(path) if File.exist?(path)) }
|
|
87
|
+
raise "generated files drifted: #{changed.join(", ")}" unless changed.empty?
|
|
88
|
+
|
|
89
|
+
Rake::Task["test:eslogger"].invoke
|
|
90
|
+
Rake::Task["test:api_surface"].invoke
|
|
91
|
+
end
|
|
92
|
+
task :sanitize do
|
|
93
|
+
sdk = `xcrun --show-sdk-path`.strip
|
|
94
|
+
ruby_headers = [RbConfig::CONFIG.fetch("rubyhdrdir"), RbConfig::CONFIG.fetch("rubyarchhdrdir")]
|
|
95
|
+
%w[address,undefined thread].each do |sanitizer|
|
|
96
|
+
output = "tmp/queue_#{sanitizer.tr(",", "_")}"
|
|
97
|
+
sh "xcrun", "clang", "-std=c11", "-Wall", "-Wextra", "-Werror", "-pthread", "-isysroot", sdk,
|
|
98
|
+
"-I#{sdk}/usr/include", "-Iext/endpoint_security", "-fsanitize=#{sanitizer}",
|
|
99
|
+
"test/native/queue_stress.c", "ext/endpoint_security/queue.c", "-o", output
|
|
100
|
+
sh output
|
|
101
|
+
|
|
102
|
+
watchdog_output = "tmp/watchdog_#{sanitizer.tr(",", "_")}"
|
|
103
|
+
sh "xcrun", "clang", "-fblocks", "-std=c11", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
|
|
104
|
+
"-pthread", "-isysroot", sdk,
|
|
105
|
+
"-I#{sdk}/usr/include", "-Iext/endpoint_security", "-Isupport/esmock",
|
|
106
|
+
*ruby_headers.flat_map { |path| ["-isystem", path] },
|
|
107
|
+
"-fsanitize=#{sanitizer}", "test/native/watchdog_stress.c", "ext/endpoint_security/watchdog.c",
|
|
108
|
+
"ext/endpoint_security/queue.c", "support/esmock/esmock.c", "-o", watchdog_output
|
|
109
|
+
sh watchdog_output
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
Rake::Task["compile:mock"].invoke
|
|
113
|
+
sanitizer_flags = "-fsanitize=undefined"
|
|
114
|
+
build_environment = { "ES_MOCK" => "1", "CFLAGS" => sanitizer_flags, "LDFLAGS" => sanitizer_flags }
|
|
115
|
+
begin
|
|
116
|
+
sh build_environment, RbConfig.ruby, "-S", "rake", "clobber", "compile"
|
|
117
|
+
sh({ "UBSAN_OPTIONS" => "halt_on_error=1" },
|
|
118
|
+
RbConfig.ruby, "-S", "rspec", "-Itest", *Dir["test/native/**/*_test.rb"])
|
|
119
|
+
ensure
|
|
120
|
+
sh({ "ES_MOCK" => "1", "CFLAGS" => ENV.fetch("CFLAGS", nil), "LDFLAGS" => ENV.fetch("LDFLAGS", nil) },
|
|
121
|
+
RbConfig.ruby, "-S", "rake", "clobber", "compile")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
task integration: ["compile:real", "test:integration:spec"]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
desc "Generate Ruby and native schema files from the active macOS SDK"
|
|
128
|
+
task :codegen do
|
|
129
|
+
ruby "codegen/run.rb"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
task test: ["test:unit", "test:native", "test:drift"]
|
|
133
|
+
|
|
134
|
+
task :rubocop do
|
|
135
|
+
sh "rubocop"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
desc "Build API documentation"
|
|
139
|
+
task :yard do
|
|
140
|
+
stats, status = Open3.capture2e("yard", "stats", "--list-undoc", "lib")
|
|
141
|
+
puts stats
|
|
142
|
+
raise "YARD coverage is below 100%" unless status.success? && stats.include?("100.00% documented")
|
|
143
|
+
|
|
144
|
+
sh "yard", "doc", "lib"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
namespace :dev do
|
|
148
|
+
desc "Diagnose the host requirements for Endpoint Security"
|
|
149
|
+
task :doctor do
|
|
150
|
+
require_relative "lib/endpoint_security/doctor"
|
|
151
|
+
EndpointSecurity::Doctor.run
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
namespace :sign do
|
|
156
|
+
desc "Copy and sign a Ruby executable with the Endpoint Security entitlement"
|
|
157
|
+
task :ruby, %i[ruby developer_id] do |_task, arguments|
|
|
158
|
+
ruby = arguments[:ruby] || RbConfig.ruby
|
|
159
|
+
identity = arguments[:developer_id] || "-"
|
|
160
|
+
sh "support/sign_ruby.sh", ruby, identity
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
desc "Run the mock delivery throughput benchmark"
|
|
165
|
+
task :bench do
|
|
166
|
+
Rake::Task["compile:mock"].invoke
|
|
167
|
+
ruby "bench/throughput.rb"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
task default: %i[compile test rubocop yard]
|
data/codegen/ast.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module EndpointSecurity
|
|
7
|
+
module Codegen
|
|
8
|
+
class AST
|
|
9
|
+
def self.load(header:, sdk:)
|
|
10
|
+
output, error, status = Open3.capture3(
|
|
11
|
+
"xcrun", "clang", "-x", "c", "-isysroot", sdk,
|
|
12
|
+
"-Xclang", "-ast-dump=json", "-fsyntax-only", "-fparse-all-comments", header
|
|
13
|
+
)
|
|
14
|
+
raise CodegenError, "clang AST extraction failed: #{error}" unless status.success?
|
|
15
|
+
|
|
16
|
+
new(JSON.parse(output))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(root)
|
|
20
|
+
@root = root
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def enum_containing(constant)
|
|
24
|
+
walk.find do |node|
|
|
25
|
+
node["kind"] == "EnumDecl" && node.fetch("inner", []).any? { |child| child["name"] == constant }
|
|
26
|
+
end || raise(CodegenError, "enum containing #{constant} was not found")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def typedef_records
|
|
30
|
+
nodes = walk.to_a
|
|
31
|
+
by_id = nodes.to_h { |node| [node["id"], node] }
|
|
32
|
+
nodes.filter_map do |node|
|
|
33
|
+
next unless node["kind"] == "TypedefDecl" && node["name"]&.start_with?("es_")
|
|
34
|
+
|
|
35
|
+
record_id = node.dig("inner", 0, "ownedTagDecl", "id")
|
|
36
|
+
record = by_id[record_id]
|
|
37
|
+
[node["name"], record] if record&.fetch("completeDefinition", false)
|
|
38
|
+
end.to_h
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def typedef_enums
|
|
42
|
+
nodes = walk.to_a
|
|
43
|
+
by_id = nodes.to_h { |node| [node["id"], node] }
|
|
44
|
+
nodes.filter_map do |node|
|
|
45
|
+
next unless node["kind"] == "TypedefDecl" && node["name"]&.start_with?("es_")
|
|
46
|
+
|
|
47
|
+
enum_id = node.dig("inner", 0, "ownedTagDecl", "id")
|
|
48
|
+
enum = by_id[enum_id]
|
|
49
|
+
[node["name"], enum] if enum&.fetch("kind", nil) == "EnumDecl"
|
|
50
|
+
end.to_h
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def walk
|
|
56
|
+
Enumerator.new do |items|
|
|
57
|
+
pending = [@root]
|
|
58
|
+
until pending.empty?
|
|
59
|
+
node = pending.pop
|
|
60
|
+
items << node
|
|
61
|
+
pending.concat(node.fetch("inner", []).reverse)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/codegen/emit_c.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EndpointSecurity
|
|
4
|
+
module Codegen
|
|
5
|
+
class EmitC
|
|
6
|
+
def initialize(records, events)
|
|
7
|
+
@records = records
|
|
8
|
+
@events = events
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def schema
|
|
12
|
+
tables = @records.map do |record|
|
|
13
|
+
next if record.fields.empty?
|
|
14
|
+
|
|
15
|
+
fields = record.fields.map do |field|
|
|
16
|
+
%( {"#{field.name}", "#{field.type}", offsetof(#{record.name}, #{field.name}), #{field.minimum_version}})
|
|
17
|
+
end.join(",\n")
|
|
18
|
+
<<~C
|
|
19
|
+
static const esrb_field_t fields_#{record.name}[] = {
|
|
20
|
+
#{fields}
|
|
21
|
+
};
|
|
22
|
+
C
|
|
23
|
+
end.join("\n")
|
|
24
|
+
|
|
25
|
+
schemas = @records.map do |record|
|
|
26
|
+
if record.fields.empty?
|
|
27
|
+
%( {"#{record.name}", NULL, 0})
|
|
28
|
+
else
|
|
29
|
+
table = "fields_#{record.name}"
|
|
30
|
+
%( {"#{record.name}", #{table}, sizeof(#{table}) / sizeof(#{table}[0])})
|
|
31
|
+
end
|
|
32
|
+
end.join(",\n")
|
|
33
|
+
|
|
34
|
+
by_name = @records.to_h { |record| [record.name, record] }
|
|
35
|
+
event_entries = @events.events.filter_map do |event|
|
|
36
|
+
member = event.symbol.to_s.sub(/\A(?:auth|notify)_/, "")
|
|
37
|
+
schema = "es_event_#{member}_t"
|
|
38
|
+
next unless by_name.key?(schema)
|
|
39
|
+
|
|
40
|
+
record = by_name.fetch("es_events_t", nil)
|
|
41
|
+
field = record&.fields&.find { |candidate| candidate.name == member }
|
|
42
|
+
next unless field
|
|
43
|
+
|
|
44
|
+
indirect = field.type.include?("*") ? "true" : "false"
|
|
45
|
+
%( {#{event.value}, offsetof(es_events_t, #{member}), "#{schema}", sizeof(#{schema}), #{indirect}})
|
|
46
|
+
end.join(",\n")
|
|
47
|
+
|
|
48
|
+
<<~C
|
|
49
|
+
/* es_schema.c -- generated by codegen/run.rb. Do not edit. */
|
|
50
|
+
#include <EndpointSecurity/EndpointSecurity.h>
|
|
51
|
+
#include <stddef.h>
|
|
52
|
+
#include "field.h"
|
|
53
|
+
|
|
54
|
+
#{tables}
|
|
55
|
+
const esrb_schema_t esrb_schemas[] = {
|
|
56
|
+
#{schemas}
|
|
57
|
+
};
|
|
58
|
+
const size_t esrb_schema_count = sizeof(esrb_schemas) / sizeof(esrb_schemas[0]);
|
|
59
|
+
|
|
60
|
+
const esrb_event_schema_t esrb_event_schemas[] = {
|
|
61
|
+
#{event_entries}
|
|
62
|
+
};
|
|
63
|
+
const size_t esrb_event_schema_count = sizeof(esrb_event_schemas) / sizeof(esrb_event_schemas[0]);
|
|
64
|
+
C
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EndpointSecurity
|
|
4
|
+
module Codegen
|
|
5
|
+
class EmitRuby
|
|
6
|
+
def initialize(event_types_ir, cacheable: [], enumerations: [])
|
|
7
|
+
@ir = event_types_ir
|
|
8
|
+
@cacheable = cacheable
|
|
9
|
+
@enumerations = enumerations
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def event_types
|
|
13
|
+
constants = @ir.events.map do |event|
|
|
14
|
+
" # The +#{event.symbol}+ event.\n #{event.symbol.upcase} = :#{event.symbol}"
|
|
15
|
+
end.join("\n")
|
|
16
|
+
pairs = @ir.events.map { |event| " #{event.symbol}: #{event.value}" }.join(",\n")
|
|
17
|
+
reserved = @ir.events.select(&:reserved).map { |event| ":#{event.symbol}" }.join(", ")
|
|
18
|
+
cacheable = @cacheable.map { |event| ":#{event}" }.join(", ")
|
|
19
|
+
|
|
20
|
+
<<~RUBY
|
|
21
|
+
# frozen_string_literal: true
|
|
22
|
+
|
|
23
|
+
# Generated by codegen/run.rb. Do not edit.
|
|
24
|
+
|
|
25
|
+
module EndpointSecurity
|
|
26
|
+
# Event names and numeric values from the build SDK.
|
|
27
|
+
module EventType
|
|
28
|
+
#{constants}
|
|
29
|
+
# Exclusive upper bound for event enum values.
|
|
30
|
+
LAST = #{@ir.last}
|
|
31
|
+
# Event symbols keyed by their SDK enum values.
|
|
32
|
+
SYMBOL_TO_VALUE = {
|
|
33
|
+
#{pairs}
|
|
34
|
+
}.freeze
|
|
35
|
+
# SDK enum values keyed by event symbol.
|
|
36
|
+
VALUE_TO_SYMBOL = SYMBOL_TO_VALUE.invert.freeze
|
|
37
|
+
# All events known to the build SDK.
|
|
38
|
+
ALL = SYMBOL_TO_VALUE.keys.freeze
|
|
39
|
+
# All authorization events.
|
|
40
|
+
ALL_AUTH = ALL.select { |event| event.to_s.start_with?("auth_") }.freeze
|
|
41
|
+
# All notification events.
|
|
42
|
+
ALL_NOTIFY = ALL.select { |event| event.to_s.start_with?("notify_") }.freeze
|
|
43
|
+
# Undocumented reserved event slots.
|
|
44
|
+
RESERVED = [#{reserved}].freeze
|
|
45
|
+
# Events for which Endpoint Security accepts cached responses.
|
|
46
|
+
CACHEABLE = [#{cacheable}].freeze
|
|
47
|
+
|
|
48
|
+
module_function
|
|
49
|
+
|
|
50
|
+
# @return [Array<Symbol>] all events
|
|
51
|
+
def all = ALL
|
|
52
|
+
# @return [Array<Symbol>] authorization events
|
|
53
|
+
def all_auth = ALL_AUTH
|
|
54
|
+
# @return [Array<Symbol>] notification events
|
|
55
|
+
def all_notify = ALL_NOTIFY
|
|
56
|
+
# @return [Boolean] whether +event+ is an authorization event
|
|
57
|
+
def auth?(event) = event.to_s.start_with?("auth_")
|
|
58
|
+
# @return [Boolean] whether +event+ is a notification event
|
|
59
|
+
def notify?(event) = event.to_s.start_with?("notify_")
|
|
60
|
+
# @return [Boolean] whether +event+ is an undocumented reserved slot
|
|
61
|
+
def reserved?(event) = RESERVED.include?(event.to_sym)
|
|
62
|
+
# @return [Boolean] whether +event+ accepts a cached response
|
|
63
|
+
def cacheable?(event) = CACHEABLE.include?(event.to_sym)
|
|
64
|
+
# @return [Boolean] whether +event+ uses a flags response
|
|
65
|
+
def flags_response?(event) = event.to_sym == :auth_open
|
|
66
|
+
# @return [Integer] SDK enum value for +event+
|
|
67
|
+
def value(event) = SYMBOL_TO_VALUE.fetch(event.to_sym)
|
|
68
|
+
# @return [Symbol, Integer] event symbol, or +value+ when unknown
|
|
69
|
+
def symbol(value) = VALUE_TO_SYMBOL.fetch(value, value)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
RUBY
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def enums
|
|
76
|
+
tables = @enumerations.map do |enumeration|
|
|
77
|
+
values = enumeration.values.map { |value| " #{value.value} => :#{value.symbol}" }.join(",\n")
|
|
78
|
+
<<~RUBY.chomp
|
|
79
|
+
"#{enumeration.name}" => {
|
|
80
|
+
#{values}
|
|
81
|
+
}.freeze
|
|
82
|
+
RUBY
|
|
83
|
+
end.join(",\n")
|
|
84
|
+
|
|
85
|
+
<<~RUBY
|
|
86
|
+
# frozen_string_literal: true
|
|
87
|
+
|
|
88
|
+
# Generated by codegen/run.rb. Do not edit.
|
|
89
|
+
|
|
90
|
+
module EndpointSecurity
|
|
91
|
+
# Symbol conversion for SDK enumeration fields.
|
|
92
|
+
module Enum
|
|
93
|
+
# Numeric-to-symbol tables keyed by SDK typedef name.
|
|
94
|
+
TABLES = {
|
|
95
|
+
#{tables}
|
|
96
|
+
}.freeze
|
|
97
|
+
|
|
98
|
+
module_function
|
|
99
|
+
|
|
100
|
+
# Returns the symbolic enum value, preserving unknown numeric values.
|
|
101
|
+
# @return [Symbol, Integer]
|
|
102
|
+
def symbol(type, value) = TABLES.fetch(type, {}).fetch(value, value)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
RUBY
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def availability
|
|
109
|
+
pairs = @ir.events.map { |event| " #{event.symbol}: \"#{event.minimum_os}\"" }.join(",\n")
|
|
110
|
+
<<~RUBY
|
|
111
|
+
# frozen_string_literal: true
|
|
112
|
+
|
|
113
|
+
# Generated by codegen/run.rb. Do not edit.
|
|
114
|
+
|
|
115
|
+
module EndpointSecurity
|
|
116
|
+
# Runtime support metadata for SDK events.
|
|
117
|
+
module Availability
|
|
118
|
+
# Minimum macOS version keyed by event symbol.
|
|
119
|
+
EVENT_MIN_OS = {
|
|
120
|
+
#{pairs}
|
|
121
|
+
}.freeze
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
RUBY
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|