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
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