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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3d8de55879068a1e6a493f92249e0fa45eb81f6c62e0d9cd394a5d5fdfaf67b
4
- data.tar.gz: 3855e4258a4f0b2f5460e3fe7e8b01aee2411768caea73e4474c948049064663
3
+ metadata.gz: 0cfb69a5bed0b27963e8ce425a3d33d666ba0ade8a66e2200bfcd60e1e20b31e
4
+ data.tar.gz: 9b47e0050d128414e54364832feb39fe2b2c1369bbc98b5577e698b614ea3f43
5
5
  SHA512:
6
- metadata.gz: 6c75e5dca4aaca6990f4345717b16d3bde23c50697d286b21db5add90c2739994a2d18713fec73ccf18fe60f1a62cefcee929c42e5f97ea2f59219e620698916
7
- data.tar.gz: 66068d2a681434dc75baaf64c9dd4cd2128fac15deacdfbb9c1551903c7b437fa9f5bcf5ff8975bc60d43e523cb175a9cf9e91ba5f4b0085613933f83a864c3c
6
+ metadata.gz: 84b6bc153690841b5248cf967895d3fac2c1a156875f5aeac99c5fde73dfa0b4f90a77cd433caf5e08903fdba09f460480fd99f3f3aa82af42da7ce344ba5dd1
7
+ data.tar.gz: a959daa6efaa89679897a7d402b3b051b1e9a289e3d9d0ad6ce3c2c2bb4e23f6cdc163df1583cd92a6f0adcd7654c159bd6ec9fc647547d6c98411340b3b0939
@@ -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
- @config.logger.info("Updated runtime settings")
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") { |event| settings_updated(event) }
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
- @config.logger.info("Updated runtime firewall list after heartbeat")
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
- @config.logger.info("Updated runtime settings after polling")
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
- @config.logger.info("Updated runtime firewall list after server-side event")
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
- # @return [Boolean]
233
- def update_settings_from_runtime_config!(data)
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.runtime_config = data
237
- Aikido::Zen.runtime_settings.update_from_runtime_config_json(data)
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
- # @return [Boolean]
245
- def update_settings_from_runtime_firewall_lists!(data)
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.runtime_firewall_lists = data
249
- Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(data)
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
@@ -2,7 +2,60 @@
2
2
 
3
3
  module Aikido::Zen
4
4
  class APICache
5
- attr_accessor :runtime_config
6
- attr_accessor :runtime_firewall_lists
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
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Aikido
4
4
  module Zen
5
- VERSION = "1.6.0.beta.2"
5
+ VERSION = "1.6.0.beta.3"
6
6
 
7
7
  # The version of libzen_internals that we build against.
8
8
  LIBZEN_VERSION = "0.1.61"
@@ -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 do
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("updated_settings", KEEPALIVE_INTERVAL)
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(settings)
80
- return unless settings
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
- @worker.every(@polling_interval, run_now: false) do
95
- update_settings(updated_settings)
96
-
97
- @config.logger.debug("Forked worker process #{Process.pid}: updated runtime settings from parent")
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
- "config" => Aikido::Zen.api_cache.runtime_config,
70
- "firewall_lists" => Aikido::Zen.api_cache.runtime_firewall_lists
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.2
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-16 00:00:00.000000000 Z
11
+ date: 2026-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby