aikido-zen 1.6.0.beta.2-x86_64-linux → 1.6.0.beta.3-x86_64-linux
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 +4 -4
- data/lib/aikido/zen/agent.rb +61 -25
- data/lib/aikido/zen/api_cache.rb +55 -2
- data/lib/aikido/zen/api_stream.rb +6 -0
- data/lib/aikido/zen/config.rb +5 -0
- data/lib/aikido/zen/version.rb +1 -1
- data/lib/aikido/zen/worker_process/agent/client.rb +47 -18
- data/lib/aikido/zen/worker_process/agent/server.rb +16 -7
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0cfb69a5bed0b27963e8ce425a3d33d666ba0ade8a66e2200bfcd60e1e20b31e
|
|
4
|
+
data.tar.gz: 9b47e0050d128414e54364832feb39fe2b2c1369bbc98b5577e698b614ea3f43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 84b6bc153690841b5248cf967895d3fac2c1a156875f5aeac99c5fde73dfa0b4f90a77cd433caf5e08903fdba09f460480fd99f3f3aa82af42da7ce344ba5dd1
|
|
7
|
+
data.tar.gz: a959daa6efaa89679897a7d402b3b051b1e9a289e3d9d0ad6ce3c2c2bb4e23f6cdc163df1583cd92a6f0adcd7654c159bd6ec9fc647547d6c98411340b3b0939
|
data/lib/aikido/zen/agent.rb
CHANGED
|
@@ -59,26 +59,36 @@ module Aikido::Zen
|
|
|
59
59
|
return
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Code coverage is disabled here because the block is only called when
|
|
63
|
+
# the process exits.
|
|
64
|
+
# :nocov:
|
|
62
65
|
at_exit { stop! if started? }
|
|
66
|
+
# :nocov:
|
|
63
67
|
|
|
64
68
|
report(Events::Started.new(time: @started_at)) do |response|
|
|
65
|
-
if update_settings_from_runtime_config!(response)
|
|
69
|
+
if update_settings_from_runtime_config!(response, reason: "after start")
|
|
66
70
|
updated_settings!
|
|
67
|
-
|
|
71
|
+
# :nocov:
|
|
72
|
+
else
|
|
73
|
+
# empty
|
|
68
74
|
end
|
|
75
|
+
# :nocov:
|
|
69
76
|
rescue => err
|
|
70
77
|
@config.logger.error(err.message)
|
|
71
78
|
end
|
|
72
79
|
|
|
73
80
|
begin
|
|
74
|
-
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
75
|
-
@config.logger.info("Updated runtime firewall list")
|
|
81
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after start")
|
|
76
82
|
rescue => err
|
|
77
83
|
@config.logger.error(err.message)
|
|
78
84
|
end
|
|
79
85
|
|
|
80
86
|
if @config.realtime_settings_updates_enabled?
|
|
81
|
-
@api_stream.handle("config-updated")
|
|
87
|
+
@api_stream.handle("config-updated") do |event|
|
|
88
|
+
@config.logger.debug("Received server-sent event: config-updated")
|
|
89
|
+
settings_updated(event)
|
|
90
|
+
end
|
|
91
|
+
|
|
82
92
|
@api_stream.start!
|
|
83
93
|
end
|
|
84
94
|
|
|
@@ -167,13 +177,15 @@ module Aikido::Zen
|
|
|
167
177
|
|
|
168
178
|
heartbeat = @collector.flush
|
|
169
179
|
report(heartbeat) do |response|
|
|
170
|
-
if update_settings_from_runtime_config!(response)
|
|
180
|
+
if update_settings_from_runtime_config!(response, reason: "after heartbeat")
|
|
171
181
|
updated_settings!
|
|
172
|
-
@config.logger.info("Updated runtime settings after heartbeat")
|
|
173
182
|
|
|
174
|
-
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
175
|
-
|
|
183
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after heartbeat")
|
|
184
|
+
# :nocov:
|
|
185
|
+
else
|
|
186
|
+
# empty
|
|
176
187
|
end
|
|
188
|
+
# :nocov:
|
|
177
189
|
end
|
|
178
190
|
end
|
|
179
191
|
|
|
@@ -187,13 +199,15 @@ module Aikido::Zen
|
|
|
187
199
|
def poll_for_setting_updates
|
|
188
200
|
@worker.every(@config.polling_interval) do
|
|
189
201
|
if @api_client.should_fetch_settings?
|
|
190
|
-
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config)
|
|
202
|
+
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config, reason: "after polling")
|
|
191
203
|
updated_settings!
|
|
192
|
-
|
|
204
|
+
# :nocov:
|
|
205
|
+
else
|
|
206
|
+
# empty
|
|
193
207
|
end
|
|
208
|
+
# :nocov:
|
|
194
209
|
|
|
195
|
-
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
196
|
-
@config.logger.info("Updated runtime firewall list after polling")
|
|
210
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after polling")
|
|
197
211
|
end
|
|
198
212
|
end
|
|
199
213
|
end
|
|
@@ -204,13 +218,15 @@ module Aikido::Zen
|
|
|
204
218
|
updated_at = Time.at(event[:data]["configUpdatedAt"].to_i)
|
|
205
219
|
|
|
206
220
|
if should_fetch_settings?(updated_at)
|
|
207
|
-
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config)
|
|
221
|
+
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config, reason: "after server-sent event")
|
|
208
222
|
updated_settings!
|
|
209
|
-
@config.logger.info("Updated runtime settings after server-side event")
|
|
210
223
|
|
|
211
|
-
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
212
|
-
|
|
224
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after server-sent event")
|
|
225
|
+
# :nocov:
|
|
226
|
+
else
|
|
227
|
+
# empty
|
|
213
228
|
end
|
|
229
|
+
# :nocov:
|
|
214
230
|
end
|
|
215
231
|
end
|
|
216
232
|
|
|
@@ -229,24 +245,44 @@ module Aikido::Zen
|
|
|
229
245
|
end
|
|
230
246
|
|
|
231
247
|
# @param data [Hash]
|
|
232
|
-
# @
|
|
233
|
-
|
|
248
|
+
# @param reason [String] context appended to the log message when logged
|
|
249
|
+
# @return [Boolean] whether the settings were updated; or nil if another
|
|
250
|
+
# thread is already applying an update.
|
|
251
|
+
def update_settings_from_runtime_config!(data, reason:)
|
|
252
|
+
# :nocov:
|
|
234
253
|
return unless @runtime_config_update_mutex.try_lock
|
|
254
|
+
# :nocov:
|
|
235
255
|
begin
|
|
236
|
-
Aikido::Zen.api_cache.
|
|
237
|
-
|
|
256
|
+
return false unless Aikido::Zen.api_cache.update_runtime_config(data)
|
|
257
|
+
|
|
258
|
+
if Aikido::Zen.runtime_settings.update_from_runtime_config_json(data)
|
|
259
|
+
@config.logger.info("Updated runtime settings #{reason}")
|
|
260
|
+
true
|
|
261
|
+
else
|
|
262
|
+
false
|
|
263
|
+
end
|
|
238
264
|
ensure
|
|
239
265
|
@runtime_config_update_mutex.unlock
|
|
240
266
|
end
|
|
241
267
|
end
|
|
242
268
|
|
|
243
269
|
# @param data [Hash]
|
|
244
|
-
# @
|
|
245
|
-
|
|
270
|
+
# @param reason [String] context appended to the log message when logged
|
|
271
|
+
# @return [Boolean] whether the settings were updated; or nil if another
|
|
272
|
+
# thread is already applying an update.
|
|
273
|
+
def update_settings_from_runtime_firewall_lists!(data, reason:)
|
|
274
|
+
# :nocov:
|
|
246
275
|
return unless @runtime_firewall_lists_update_mutex.try_lock
|
|
276
|
+
# :nocov:
|
|
247
277
|
begin
|
|
248
|
-
Aikido::Zen.api_cache.
|
|
249
|
-
|
|
278
|
+
return false unless Aikido::Zen.api_cache.update_runtime_firewall_lists(data)
|
|
279
|
+
|
|
280
|
+
if Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(data)
|
|
281
|
+
@config.logger.info("Updated runtime firewall list #{reason}")
|
|
282
|
+
true
|
|
283
|
+
else
|
|
284
|
+
false
|
|
285
|
+
end
|
|
250
286
|
ensure
|
|
251
287
|
@runtime_firewall_lists_update_mutex.unlock
|
|
252
288
|
end
|
data/lib/aikido/zen/api_cache.rb
CHANGED
|
@@ -2,7 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
module Aikido::Zen
|
|
4
4
|
class APICache
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
attr_reader :runtime_config
|
|
6
|
+
attr_reader :runtime_firewall_lists
|
|
7
|
+
attr_reader :runtime_config_generation
|
|
8
|
+
attr_reader :runtime_firewall_lists_generation
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@runtime_config_generation = 0
|
|
12
|
+
@runtime_firewall_lists_generation = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def runtime_config=(value)
|
|
16
|
+
update_runtime_config(value)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def runtime_firewall_lists=(value)
|
|
20
|
+
update_runtime_firewall_lists(value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param value [Hash, nil]
|
|
24
|
+
# @return [Boolean] whether the value actually changed
|
|
25
|
+
def update_runtime_config(value)
|
|
26
|
+
return false if value == @runtime_config
|
|
27
|
+
|
|
28
|
+
@runtime_config_generation += 1
|
|
29
|
+
@runtime_config = value
|
|
30
|
+
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param value [Hash, nil]
|
|
35
|
+
# @return [Boolean] whether the value actually changed
|
|
36
|
+
def update_runtime_firewall_lists(value)
|
|
37
|
+
return false if value == @runtime_firewall_lists
|
|
38
|
+
|
|
39
|
+
@runtime_firewall_lists_generation += 1
|
|
40
|
+
@runtime_firewall_lists = value
|
|
41
|
+
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param known_generation [Integer, nil]
|
|
46
|
+
# @return [Array(Object, Integer), nil] the current value and generation,
|
|
47
|
+
# or nil if the known generation is already current.
|
|
48
|
+
def config_if_changed(known_generation)
|
|
49
|
+
return nil if known_generation == runtime_config_generation
|
|
50
|
+
[runtime_config, runtime_config_generation]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param known_generation [Integer, nil]
|
|
54
|
+
# @return [Array(Object, Integer), nil] the current value and generation,
|
|
55
|
+
# or nil if the known generation is already current.
|
|
56
|
+
def firewall_lists_if_changed(known_generation)
|
|
57
|
+
return nil if known_generation == runtime_firewall_lists_generation
|
|
58
|
+
[runtime_firewall_lists, runtime_firewall_lists_generation]
|
|
59
|
+
end
|
|
7
60
|
end
|
|
8
61
|
end
|
|
@@ -116,12 +116,18 @@ module Aikido::Zen
|
|
|
116
116
|
http.open_timeout = @open_timeout
|
|
117
117
|
http.write_timeout = @write_timeout
|
|
118
118
|
http.read_timeout = @read_timeout
|
|
119
|
+
# Working around Net::HTTP cleverness; retries GET requests once by default.
|
|
119
120
|
http.max_retries = 0
|
|
120
121
|
|
|
121
122
|
request = Net::HTTP::Get.new("/api/runtime/stream")
|
|
122
123
|
request["Authorization"] = @token
|
|
123
124
|
request["Accept"] = "text/event-stream"
|
|
124
125
|
request["Cache-Control"] = "no-cache"
|
|
126
|
+
request["X-Agent-Platform"] = "ruby"
|
|
127
|
+
request["X-Agent-Version"] = Aikido::Zen::VERSION
|
|
128
|
+
# Working around Net::HTTP cleverness; auto-negotiates gzip and buffers
|
|
129
|
+
# decompressed output internally.
|
|
130
|
+
request["Accept-Encoding"] = "identity"
|
|
125
131
|
|
|
126
132
|
@config.logger.debug("API stream connecting")
|
|
127
133
|
http.start
|
data/lib/aikido/zen/config.rb
CHANGED
|
@@ -60,6 +60,10 @@ module Aikido::Zen
|
|
|
60
60
|
# poll the parent process for updated runtime settings. Defaults to 10 seconds.
|
|
61
61
|
attr_accessor :worker_process_polling_interval
|
|
62
62
|
|
|
63
|
+
# @return [Integer] the maximum random delay in seconds before a forked
|
|
64
|
+
# worker process starts polling. Defaults to 10 seconds.
|
|
65
|
+
attr_accessor :worker_process_polling_jitter
|
|
66
|
+
|
|
63
67
|
# @return [Integer] the interval in seconds at which forked worker processes
|
|
64
68
|
# flush their collected stats to the parent process. Defaults to 10 seconds.
|
|
65
69
|
attr_accessor :worker_process_heartbeat_interval
|
|
@@ -237,6 +241,7 @@ module Aikido::Zen
|
|
|
237
241
|
self.polling_interval = 60 # 1 min
|
|
238
242
|
self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
|
|
239
243
|
self.worker_process_polling_interval = 10
|
|
244
|
+
self.worker_process_polling_jitter = 10
|
|
240
245
|
self.worker_process_heartbeat_interval = 10
|
|
241
246
|
self.json_encoder = DEFAULT_JSON_ENCODER
|
|
242
247
|
self.json_decoder = DEFAULT_JSON_DECODER
|
data/lib/aikido/zen/version.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Aikido::Zen::WorkerProcess
|
|
|
16
16
|
worker: Aikido::Zen::Worker.new(config: config),
|
|
17
17
|
keepalive_worker: Aikido::Zen::Worker.new(config: config),
|
|
18
18
|
polling_interval: config.worker_process_polling_interval,
|
|
19
|
+
polling_jitter: config.worker_process_polling_jitter,
|
|
19
20
|
heartbeat_interval: config.worker_process_heartbeat_interval,
|
|
20
21
|
collector: Aikido::Zen.collector
|
|
21
22
|
)
|
|
@@ -23,10 +24,14 @@ module Aikido::Zen::WorkerProcess
|
|
|
23
24
|
@worker = worker
|
|
24
25
|
@keepalive_worker = keepalive_worker
|
|
25
26
|
@polling_interval = polling_interval
|
|
27
|
+
@polling_jitter = polling_jitter
|
|
26
28
|
@heartbeat_interval = heartbeat_interval
|
|
27
29
|
@collector = collector
|
|
28
30
|
|
|
29
31
|
@rpc_client = Aikido::Zen::RPC::Client.new(secret, host, port, reconnect: true)
|
|
32
|
+
|
|
33
|
+
@known_config_generation = nil
|
|
34
|
+
@known_firewall_lists_generation = nil
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
def close
|
|
@@ -34,14 +39,7 @@ module Aikido::Zen::WorkerProcess
|
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
def start
|
|
37
|
-
@rpc_client.start
|
|
38
|
-
@worker.perform do
|
|
39
|
-
update_settings(updated_settings)
|
|
40
|
-
rescue => err
|
|
41
|
-
@config.logger.error("Forked worker process #{Process.pid}: failed to get settings after connecting to parent: #{err.message}")
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
42
|
+
@rpc_client.start
|
|
45
43
|
schedule_tasks
|
|
46
44
|
end
|
|
47
45
|
|
|
@@ -73,35 +71,66 @@ module Aikido::Zen::WorkerProcess
|
|
|
73
71
|
private
|
|
74
72
|
|
|
75
73
|
def updated_settings
|
|
76
|
-
@rpc_client.invoke(
|
|
74
|
+
@rpc_client.invoke(
|
|
75
|
+
"updated_settings",
|
|
76
|
+
KEEPALIVE_INTERVAL,
|
|
77
|
+
@known_config_generation,
|
|
78
|
+
@known_firewall_lists_generation
|
|
79
|
+
)
|
|
77
80
|
end
|
|
78
81
|
|
|
79
|
-
def update_settings
|
|
80
|
-
|
|
82
|
+
def update_settings
|
|
83
|
+
settings = updated_settings
|
|
84
|
+
return false unless settings
|
|
85
|
+
return false unless settings["config"] || settings["firewall_lists"]
|
|
86
|
+
|
|
87
|
+
@config.logger.debug("Forked worker process #{Process.pid}: starting runtime settings update")
|
|
81
88
|
|
|
82
89
|
if settings["config"]
|
|
90
|
+
@config.logger.debug("Forked worker process #{Process.pid}: starting config update")
|
|
83
91
|
Aikido::Zen.runtime_settings.update_from_runtime_config_json(settings["config"])
|
|
92
|
+
@known_config_generation = settings["config_generation"]
|
|
93
|
+
@config.logger.debug("Forked worker process #{Process.pid}: finished config update")
|
|
84
94
|
end
|
|
85
95
|
|
|
86
96
|
if settings["firewall_lists"]
|
|
97
|
+
@config.logger.debug("Forked worker process #{Process.pid}: starting firewall_lists update")
|
|
87
98
|
Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(settings["firewall_lists"])
|
|
99
|
+
@known_firewall_lists_generation = settings["firewall_lists_generation"]
|
|
100
|
+
@config.logger.debug("Forked worker process #{Process.pid}: finished firewall_lists update")
|
|
88
101
|
end
|
|
102
|
+
|
|
103
|
+
@config.logger.debug("Forked worker process #{Process.pid}: finished runtime settings update")
|
|
104
|
+
|
|
105
|
+
true
|
|
106
|
+
rescue => err
|
|
107
|
+
@config.logger.error("Forked worker process #{Process.pid}: failed to get settings from parent: #{err.message}")
|
|
108
|
+
false
|
|
89
109
|
end
|
|
90
110
|
|
|
91
111
|
def schedule_tasks
|
|
92
112
|
@keepalive_worker.every(KEEPALIVE_INTERVAL, run_now: false) { keepalive }
|
|
93
113
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
rescue => err
|
|
99
|
-
@config.logger.error("Forked worker process #{Process.pid}: failed to get settings from parent: #{err.message}")
|
|
100
|
-
end
|
|
114
|
+
# Delay start polling to reduce the chance of workers polling in lockstep.
|
|
115
|
+
delay = polling_start_delay
|
|
116
|
+
@config.logger.debug("Forked worker process #{Process.pid}: will start polling in #{delay}s")
|
|
117
|
+
@worker.delay(delay) { start_polling }
|
|
101
118
|
|
|
102
119
|
@worker.every(@heartbeat_interval, run_now: false) { send_collector_events }
|
|
103
120
|
end
|
|
104
121
|
|
|
122
|
+
def start_polling
|
|
123
|
+
@config.logger.debug("Forked worker process #{Process.pid}: started polling")
|
|
124
|
+
@worker.every(@polling_interval, run_now: true) { update_settings }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def polling_start_delay
|
|
128
|
+
return 0 if @polling_jitter < 1
|
|
129
|
+
|
|
130
|
+
# Seed by pid because forked processes inherit identical PRNG state.
|
|
131
|
+
Random.new(Process.pid).rand(1..@polling_jitter)
|
|
132
|
+
end
|
|
133
|
+
|
|
105
134
|
def keepalive
|
|
106
135
|
@rpc_client.invoke("ping", KEEPALIVE_INTERVAL)
|
|
107
136
|
rescue => err
|
|
@@ -12,8 +12,8 @@ module Aikido::Zen::WorkerProcess
|
|
|
12
12
|
respond.call(nil)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
@rpc_server.handle("updated_settings") do |respond|
|
|
16
|
-
respond.call(updated_settings)
|
|
15
|
+
@rpc_server.handle("updated_settings") do |respond, known_config_generation = nil, known_firewall_lists_generation = nil|
|
|
16
|
+
respond.call(updated_settings(known_config_generation, known_firewall_lists_generation))
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
@rpc_server.handle("send_collector_events") do |respond, events_data|
|
|
@@ -64,11 +64,20 @@ module Aikido::Zen::WorkerProcess
|
|
|
64
64
|
# Visible for testing.
|
|
65
65
|
RequestKind = Struct.new(:route, :schema, :client_ip, :actor)
|
|
66
66
|
|
|
67
|
-
def updated_settings
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
def updated_settings(known_config_generation = nil, known_firewall_lists_generation = nil)
|
|
68
|
+
result = {}
|
|
69
|
+
|
|
70
|
+
config_change = Aikido::Zen.api_cache.config_if_changed(known_config_generation)
|
|
71
|
+
if config_change
|
|
72
|
+
result["config"], result["config_generation"] = config_change
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
firewall_lists_change = Aikido::Zen.api_cache.firewall_lists_if_changed(known_firewall_lists_generation)
|
|
76
|
+
if firewall_lists_change
|
|
77
|
+
result["firewall_lists"], result["firewall_lists_generation"] = firewall_lists_change
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
result
|
|
72
81
|
end
|
|
73
82
|
|
|
74
83
|
def send_collector_events(events_data)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aikido-zen
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.6.0.beta.
|
|
4
|
+
version: 1.6.0.beta.3
|
|
5
5
|
platform: x86_64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Aikido Security
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|