aikido-zen 1.4.1 → 1.5.0

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: ea0d03f79f4de77618d118402c5e77d661f595d711651c1776cbba978323c3dc
4
- data.tar.gz: 8c01f5d718ce07261e84351db938b8b5bebf3a4bf7b9fc1a4c691686adb3d670
3
+ metadata.gz: 2a9cc243a2ca18ad42f75596db9fb9558362371ee68646f2f75997b80dfd9d04
4
+ data.tar.gz: 7f9990113775ca615b9b50035a15779e5a9ceb777ad793c164dc6b69c8898732
5
5
  SHA512:
6
- metadata.gz: e01c0bfada461c9884c0846c3ddebd949cb803767018fcfcb12d0c7f36e27ae4eebc9f84269b73970c984754b326645e7139dd788ed030e7d851cdfb7ae93fb3
7
- data.tar.gz: 9a5fcf35a47e825ce3b39dd556cf58a68feb49d1eee79b9e9ea96e0bf9afe42f4f6e00948d33c23c9fe31f0d7c40651afb1681406a7f63e7055d6354f89fdcc9
6
+ metadata.gz: 0f4711a8dacc3597ea2a143cb50e735f3234af4d8fcfa0f3d76bf993de51dc17a19c6967c338119cb0433d2c6a850a363a96a6326259316c9d2fc5d346c3809c
7
+ data.tar.gz: 1d85b479341bec9f2a4ad9b9b8bf12bfc71dbd98213a6020b8c03fde0ea3a6cea47c9e1a6c0f5895474c8e8de3cac823d655cf59396c16309640a45c976cb68b
@@ -21,15 +21,20 @@ module Aikido::Zen
21
21
  collector: Aikido::Zen.collector,
22
22
  detached_agent: Aikido::Zen.detached_agent,
23
23
  worker: Aikido::Zen::Worker.new(config: config),
24
- api_client: Aikido::Zen::APIClient.new(config: config)
24
+ api_client: Aikido::Zen::APIClient.new(config: config),
25
+ api_stream: Aikido::Zen::APIStream.new(config: config)
25
26
  )
26
- @started_at = nil
27
-
28
27
  @config = config
29
- @worker = worker
30
- @api_client = api_client
31
28
  @collector = collector
32
29
  @detached_agent = detached_agent
30
+ @worker = worker
31
+ @api_client = api_client
32
+ @api_stream = api_stream
33
+
34
+ @started_at = nil
35
+
36
+ @runtime_config_update_mutex = Mutex.new
37
+ @runtime_firewall_lists_update_mutex = Mutex.new
33
38
  end
34
39
 
35
40
  def started?
@@ -59,7 +64,7 @@ module Aikido::Zen
59
64
  at_exit { stop! if started? }
60
65
 
61
66
  report(Events::Started.new(time: @started_at)) do |response|
62
- if Aikido::Zen.runtime_settings.update_from_runtime_config_json(response)
67
+ if update_settings_from_runtime_config!(response)
63
68
  updated_settings!
64
69
  @config.logger.info("Updated runtime settings")
65
70
  end
@@ -68,12 +73,25 @@ module Aikido::Zen
68
73
  end
69
74
 
70
75
  begin
71
- Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(@api_client.fetch_runtime_firewall_lists)
76
+ update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
72
77
  @config.logger.info("Updated runtime firewall list")
73
78
  rescue => err
74
79
  @config.logger.error(err.message)
75
80
  end
76
81
 
82
+ if @config.realtime_settings_updates_enabled?
83
+ if @api_stream.can_connect?
84
+ @api_stream.handle("config-updated") { |event| settings_updated(event) }
85
+ @api_stream.start!
86
+
87
+ # Use the realtime setting updates endpoint when polling to check
88
+ # whether settings should be fetched.
89
+ @api_client.should_fetch_settings_endpoint = @config.realtime_settings_updates_endpoint
90
+ else
91
+ @config.logger.warn("Can't reach #{Aikido::Zen.config.realtime_settings_updates_endpoint}, make sure it's in your outbound firewall allowlist. Realtime config updates won't be available, switched to polling.")
92
+ end
93
+ end
94
+
77
95
  poll_for_setting_updates
78
96
 
79
97
  @config.initial_heartbeat_delays.each do |heartbeat_delay|
@@ -92,6 +110,8 @@ module Aikido::Zen
92
110
  @config.logger.info("Stopping Aikido agent")
93
111
  @started_at = nil
94
112
  @worker.shutdown
113
+
114
+ @api_stream.stop!
95
115
  end
96
116
 
97
117
  # Respond to the runtime settings changing after being fetched from the
@@ -157,11 +177,11 @@ module Aikido::Zen
157
177
 
158
178
  heartbeat = @collector.flush
159
179
  report(heartbeat) do |response|
160
- if Aikido::Zen.runtime_settings.update_from_runtime_config_json(response)
180
+ if update_settings_from_runtime_config!(response)
161
181
  updated_settings!
162
182
  @config.logger.info("Updated runtime settings after heartbeat")
163
183
 
164
- Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(@api_client.fetch_runtime_firewall_lists)
184
+ update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
165
185
  @config.logger.info("Updated runtime firewall list after heartbeat")
166
186
  end
167
187
  end
@@ -177,23 +197,68 @@ module Aikido::Zen
177
197
  def poll_for_setting_updates
178
198
  @worker.every(@config.polling_interval) do
179
199
  if @api_client.should_fetch_settings?
180
- if Aikido::Zen.runtime_settings.update_from_runtime_config_json(@api_client.fetch_runtime_config)
200
+ if update_settings_from_runtime_config!(@api_client.fetch_runtime_config)
181
201
  updated_settings!
182
202
  @config.logger.info("Updated runtime settings after polling")
183
203
  end
184
204
 
185
- Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(@api_client.fetch_runtime_firewall_lists)
205
+ update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
186
206
  @config.logger.info("Updated runtime firewall list after polling")
187
207
  end
188
208
  end
189
209
  end
190
210
 
191
- private def heartbeats
211
+ private
212
+
213
+ def settings_updated(event)
214
+ updated_at = Time.at(event[:data]["configUpdatedAt"].to_i)
215
+
216
+ if should_fetch_settings?(updated_at)
217
+ if update_settings_from_runtime_config!(@api_client.fetch_runtime_config)
218
+ updated_settings!
219
+ @config.logger.info("Updated runtime settings after server-side event")
220
+
221
+ update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
222
+ @config.logger.info("Updated runtime firewall list after server-side event")
223
+ end
224
+ end
225
+ end
226
+
227
+ def should_fetch_settings?(updated_at, last_updated_at = Aikido::Zen.runtime_settings.updated_at)
228
+ return false unless @api_client.can_make_requests?
229
+ return true if last_updated_at.nil?
230
+
231
+ updated_at > last_updated_at
232
+ end
233
+
234
+ def heartbeats
192
235
  @heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
193
236
  config: @config,
194
237
  worker: @worker
195
238
  )
196
239
  end
240
+
241
+ # @param data [Hash]
242
+ # @return [Boolean]
243
+ def update_settings_from_runtime_config!(data)
244
+ return unless @runtime_config_update_mutex.try_lock
245
+ begin
246
+ Aikido::Zen.runtime_settings.update_from_runtime_config_json(data)
247
+ ensure
248
+ @runtime_config_update_mutex.unlock
249
+ end
250
+ end
251
+
252
+ # @param data [Hash]
253
+ # @return [Boolean]
254
+ def update_settings_from_runtime_firewall_lists!(data)
255
+ return unless @runtime_firewall_lists_update_mutex.try_lock
256
+ begin
257
+ Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(data)
258
+ ensure
259
+ @runtime_firewall_lists_update_mutex.unlock
260
+ end
261
+ end
197
262
  end
198
263
  end
199
264
 
@@ -16,6 +16,12 @@ module Aikido::Zen
16
16
  @config = config
17
17
  @system_info = system_info
18
18
  @rate_limiter = rate_limiter
19
+
20
+ @should_fetch_settings_endpoint = @config.realtime_endpoint
21
+ end
22
+
23
+ def should_fetch_settings_endpoint=(uri)
24
+ @should_fetch_settings_endpoint = URI(uri)
19
25
  end
20
26
 
21
27
  # @return [Boolean] whether we have a configured token.
@@ -38,10 +44,10 @@ module Aikido::Zen
38
44
 
39
45
  response = request(
40
46
  Net::HTTP::Get.new("/config", default_headers),
41
- base_url: @config.realtime_endpoint
47
+ base_url: @should_fetch_settings_endpoint
42
48
  )
43
49
 
44
- new_updated_at = Time.at(response["configUpdatedAt"].to_i / 1000)
50
+ new_updated_at = Time.at(response["configUpdatedAt"].to_i)
45
51
  new_updated_at > last_updated_at
46
52
  end
47
53
 
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module Aikido::Zen
8
+ class APIStream
9
+ def initialize(
10
+ config: Aikido::Zen.config,
11
+ min_backoff: 5,
12
+ max_backoff: 60,
13
+ backoff_reset: 30,
14
+ open_timeout: 5,
15
+ write_timeout: open_timeout,
16
+ read_timeout: 70
17
+ )
18
+ @config = config
19
+ @min_backoff = min_backoff
20
+ @max_backoff = max_backoff
21
+ @backoff_reset = backoff_reset
22
+ @open_timeout = open_timeout
23
+ @write_timeout = write_timeout
24
+ @read_timeout = read_timeout
25
+
26
+ @running = Concurrent::AtomicBoolean.new
27
+ @thread = nil
28
+ @http = nil
29
+
30
+ endpoint = @config.realtime_settings_updates_endpoint
31
+
32
+ @host = endpoint.host
33
+ @port = endpoint.port
34
+ @use_ssl = endpoint.scheme == "https"
35
+ @token = @config.api_token
36
+
37
+ @handlers = Concurrent::Array.new
38
+ end
39
+
40
+ # @return [Boolean] whether we could connect to the realtime endpoint
41
+ def can_connect?
42
+ http = Net::HTTP.new(@host, @port)
43
+ http.use_ssl = @use_ssl
44
+ http.open_timeout = 5
45
+ http.write_timeout = 5
46
+ http.read_timeout = 5
47
+ http.max_retries = 0
48
+
49
+ request = Net::HTTP::Get.new("/config")
50
+ request["Authorization"] = @token
51
+
52
+ begin
53
+ http.request(request)
54
+
55
+ return true
56
+ rescue Timeout::Error, SocketError, IOError, SystemCallError, OpenSSL::OpenSSLError => err
57
+ @config.logger.debug("Error probing realtime endpoint: #{err.class}: #{err.message}")
58
+ rescue => err
59
+ @config.logger.error("Error probing realtime endpoint: #{err.class}: #{err.message}")
60
+ end
61
+
62
+ false
63
+ end
64
+
65
+ def running?
66
+ @running.true?
67
+ end
68
+ alias_method :started?, :running?
69
+
70
+ def start!
71
+ return false unless @running.make_true
72
+
73
+ @thread = Thread.new do
74
+ backoff = @min_backoff
75
+
76
+ while running?
77
+ time_before = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
78
+
79
+ begin
80
+ work
81
+ rescue IOError => err
82
+ @config.logger.debug("Error in API stream: #{err.class}: #{err.message}") if running?
83
+ rescue Timeout::Error, SocketError, SystemCallError, OpenSSL::OpenSSLError => err
84
+ @config.logger.debug("Error in API stream: #{err.class}: #{err.message}")
85
+ rescue => err
86
+ @config.logger.error("Error in API stream: #{err.class}: #{err.message}")
87
+ end
88
+
89
+ break unless running?
90
+
91
+ time_after = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
92
+
93
+ backoff = if time_after - time_before > @backoff_reset
94
+ @min_backoff
95
+ else
96
+ [backoff * 2, @max_backoff].min
97
+ end
98
+
99
+ jitter = rand * backoff / 2
100
+
101
+ @config.logger.debug("API stream reconnecting in %d seconds" % (backoff + jitter).ceil)
102
+
103
+ sleep(backoff + jitter)
104
+ end
105
+ end
106
+
107
+ true
108
+ end
109
+
110
+ def stop!
111
+ return false unless @running.make_false
112
+
113
+ begin
114
+ @http&.finish
115
+ rescue IOError
116
+ # ignore error
117
+ end
118
+
119
+ begin
120
+ @thread&.wakeup
121
+ rescue ThreadError
122
+ # ignore error
123
+ end
124
+
125
+ @thread.join(@read_timeout)
126
+
127
+ true
128
+ end
129
+
130
+ def handle(type, &block)
131
+ raise ArgumentError, "block required" unless block
132
+
133
+ @handlers << proc do |event|
134
+ block.call(event) if type === event[:type]
135
+ end
136
+ end
137
+
138
+ private def work
139
+ http = Net::HTTP.new(@host, @port)
140
+ http.use_ssl = @use_ssl
141
+ http.open_timeout = @open_timeout
142
+ http.write_timeout = @write_timeout
143
+ http.read_timeout = @read_timeout
144
+ http.max_retries = 0
145
+
146
+ request = Net::HTTP::Get.new("/api/runtime/stream")
147
+ request["Authorization"] = @token
148
+ request["Accept"] = "text/event-stream"
149
+ request["Cache-Control"] = "no-cache"
150
+
151
+ @config.logger.debug("API stream connecting")
152
+ http.start
153
+ @config.logger.debug("API stream connected")
154
+
155
+ @http = http
156
+
157
+ begin
158
+ http.request(request) do |response|
159
+ case response.code.to_i
160
+ when 200
161
+ # empty
162
+ when 401, 403
163
+ @running.make_false
164
+ return nil
165
+ else
166
+ return nil
167
+ end
168
+
169
+ buffer = +""
170
+
171
+ response.read_body do |chunk|
172
+ return nil unless running?
173
+
174
+ @config.logger.debug("API stream received chunk of #{chunk.bytesize} bytes")
175
+
176
+ buffer << chunk
177
+
178
+ while (index = buffer.index("\n\n"))
179
+ event_str = buffer.slice!(0..index + 1)
180
+ buffer = buffer.lstrip
181
+
182
+ event = {}
183
+
184
+ begin
185
+ event_str.each_line do |line|
186
+ case line
187
+ when /^event:\s*(.+)/
188
+ event[:type] = $1.strip
189
+ when /^data:\s*(.+)/
190
+ event[:data] = JSON.parse($1.strip)
191
+ end
192
+ end
193
+ rescue => err
194
+ @config.logger.error("Error in API stream: #{err.class}: #{err.message}")
195
+ next
196
+ end
197
+
198
+ @handlers.each do |handler|
199
+ handler.call(event)
200
+ rescue => err
201
+ @config.logger.error("Error in API stream: #{err.class}: #{err.message}")
202
+ end
203
+ end
204
+ end
205
+ end
206
+ ensure
207
+ @config.logger.debug("API stream disconnecting")
208
+
209
+ @http = nil
210
+
211
+ http.finish if http.started?
212
+
213
+ @config.logger.debug("API stream disconnected")
214
+ end
215
+ end
216
+ end
217
+ end
@@ -1,27 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module Aikido::Zen
4
6
  module AttackWave
5
7
  module Helpers
6
- def self.web_scanner?(context)
7
- return true if suspicious_request?(context)
8
+ def self.web_scanner?(context, status_code)
9
+ return true if suspicious_request?(context, status_code)
8
10
 
9
11
  return true if include_suspicious_payload?(context)
10
12
 
11
13
  false
12
14
  end
13
15
 
14
- def self.suspicious_request?(context)
16
+ def self.suspicious_request?(context, status_code)
15
17
  request = context.request
16
18
 
17
- suspicious_method?(request.request_method) || suspicious_path?(request.path_info)
19
+ suspicious_method?(request.request_method) || suspicious_path?(request.path_info, status_code)
18
20
  end
19
21
 
20
22
  def self.suspicious_method?(method)
21
23
  SUSPICIOUS_METHODS.include?(method.downcase)
22
24
  end
23
25
 
24
- def self.suspicious_path?(path)
26
+ def self.suspicious_path?(path, status_code)
25
27
  path_parts = path.downcase.split("/")
26
28
 
27
29
  file_name = path_parts.pop if path_parts.length > 0
@@ -34,6 +36,8 @@ module Aikido::Zen
34
36
  file_extension = file_name_parts.pop if file_name_parts.length > 1
35
37
 
36
38
  return true if SUSPICIOUS_FILE_EXTENSIONS.include?(file_extension)
39
+
40
+ return true if FOREIGN_EXTENSIONS.include?(file_extension) && status_code == 404
37
41
  end
38
42
 
39
43
  path_parts.any? do |directory_name|
@@ -434,6 +438,11 @@ module Aikido::Zen
434
438
  "sqlite3db"
435
439
  ].map(&:downcase).freeze
436
440
 
441
+ # Extensions that a Ruby app would not natively serve. Requests to these
442
+ # paths are only treated as scan hits when the response is 404 — a 200
443
+ # may indicate the app is proxying to a PHP/Java backend.
444
+ FOREIGN_EXTENSIONS = Set.new(%w[php php3 php4 php5 phtml java jsp jspx]).freeze
445
+
437
446
  SUSPICIOUS_SQL_KEYWORDS = [
438
447
  "SELECT (CASE WHEN",
439
448
  "SELECT COUNT(",
@@ -21,14 +21,14 @@ module Aikido::Zen
21
21
  end
22
22
  end
23
23
 
24
- def attack_wave?(context)
24
+ def attack_wave?(context, status_code = nil)
25
25
  client_ip = context.request.client_ip
26
26
 
27
27
  return false unless client_ip
28
28
 
29
29
  return false if @event_times[client_ip]
30
30
 
31
- return false unless AttackWave::Helpers.web_scanner?(context)
31
+ return false unless AttackWave::Helpers.web_scanner?(context, status_code)
32
32
 
33
33
  request_count = @request_counts[client_ip] += 1
34
34
 
@@ -216,6 +216,15 @@ module Aikido::Zen
216
216
  # Defaults to 1000 entries.
217
217
  attr_accessor :idor_max_cache_entries
218
218
 
219
+ # @return [Boolean] whether the realtime settings updates feature is enabled.
220
+ # Defaults to false.
221
+ attr_accessor :realtime_settings_updates_enabled
222
+ alias_method :realtime_settings_updates_enabled?, :realtime_settings_updates_enabled
223
+
224
+ # @return [URI] The HTTP host for realtime settings updates.
225
+ # Defaults to +https://zen.aikido.dev+.
226
+ attr_reader :realtime_settings_updates_endpoint
227
+
219
228
  def initialize
220
229
  self.insert_middleware_after = ::ActionDispatch::RemoteIp
221
230
  self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
@@ -261,6 +270,8 @@ module Aikido::Zen
261
270
  self.idor_tenant_column_name = nil
262
271
  self.idor_excluded_table_names = []
263
272
  self.idor_max_cache_entries = 1000
273
+ self.realtime_settings_updates_enabled = false
274
+ self.realtime_settings_updates_endpoint = ENV.fetch("AIKIDO_REALTIME_SETTINGS_UPDATES_ENDPOINT", DEFAULT_REALTIME_SETTINGS_UPDATES_BASE_URL)
264
275
  end
265
276
 
266
277
  # Set the base URL for API requests.
@@ -277,6 +288,13 @@ module Aikido::Zen
277
288
  @realtime_endpoint = URI(url)
278
289
  end
279
290
 
291
+ # Set the base URL for the realtime settings updates feature.
292
+ #
293
+ # @param url [String, URI]
294
+ def realtime_settings_updates_endpoint=(url)
295
+ @realtime_settings_updates_endpoint = URI(url)
296
+ end
297
+
280
298
  # Set the logger and configure its severity level according to agent's debug mode
281
299
  # @param logger [::Logger]
282
300
  def logger=(logger)
@@ -346,6 +364,9 @@ module Aikido::Zen
346
364
  # @!visibility private
347
365
  DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
348
366
 
367
+ # @!visibility private
368
+ DEFAULT_REALTIME_SETTINGS_UPDATES_BASE_URL = "https://zen.aikido.dev"
369
+
349
370
  # @!visibility private
350
371
  DEFAULT_JSON_ENCODER = JSON.method(:dump)
351
372
 
@@ -12,28 +12,29 @@ module Aikido
12
12
 
13
13
  def call(env)
14
14
  response = @app.call(env)
15
+ status_code = response[0].to_i
15
16
 
16
17
  context = @zen.current_context
17
- protect(context)
18
+ protect(context, status_code)
18
19
 
19
20
  response
20
21
  end
21
22
 
22
23
  # @api private
23
24
  # Visible for testing.
24
- def attack_wave?(context)
25
+ def attack_wave?(context, status_code = nil)
25
26
  request = context.request
26
27
  return false if request.nil?
27
28
 
28
29
  return false if @settings.bypassed_ips.include?(request.client_ip)
29
30
 
30
- @zen.attack_wave_detector.attack_wave?(context)
31
+ @zen.attack_wave_detector.attack_wave?(context, status_code)
31
32
  end
32
33
 
33
34
  # @api private
34
35
  # Visible for testing.
35
- def protect(context)
36
- if attack_wave?(context)
36
+ def protect(context, status_code = nil)
37
+ if attack_wave?(context, status_code)
37
38
  client_ip = context.request.client_ip
38
39
 
39
40
  request = Aikido::Zen::AttackWave::Request.new(
@@ -80,11 +80,11 @@ module Aikido::Zen
80
80
  #
81
81
  # @param data [Hash] the decoded JSON payload from the /api/runtime/config
82
82
  # API endpoint.
83
- # @return [bool]
83
+ # @return [Boolean]
84
84
  def update_from_runtime_config_json(data)
85
85
  last_updated_at = updated_at
86
86
 
87
- self.updated_at = Time.at(data["configUpdatedAt"].to_i / 1000)
87
+ self.updated_at = Time.at(data["configUpdatedAt"].to_i)
88
88
  self.heartbeat_interval = data["heartbeatIntervalInMS"].to_i / 1000
89
89
  self.endpoints = RuntimeSettings::Endpoints.from_json(data["endpoints"])
90
90
  self.blocked_user_ids = data["blockedUserIds"]
@@ -105,7 +105,7 @@ module Aikido::Zen
105
105
  #
106
106
  # @param data [Hash] the decoded JSON payload from the /api/runtime/firewall/lists
107
107
  # API endpoint.
108
- # @return [void]
108
+ # @return [Boolean]
109
109
  def update_from_runtime_firewall_lists_json(data)
110
110
  self.blocked_user_agent_regexp = pattern(data["blockedUserAgents"])
111
111
 
@@ -142,6 +142,8 @@ module Aikido::Zen
142
142
  data["monitoredIPAddresses"]&.each do |ip_list|
143
143
  monitored_ip_lists << RuntimeSettings::IPList.from_json(ip_list)
144
144
  end
145
+
146
+ true
145
147
  end
146
148
 
147
149
  # Construct a regular expression from the non-nil and non-empty string,
@@ -45,6 +45,9 @@ module Aikido::Zen
45
45
  end
46
46
 
47
47
  def attack?
48
+ # We block home-relative path
49
+ return true if @input.start_with?("~") && @filepath.start_with?(@input)
50
+
48
51
  # Single character are ignored because they don't pose a big threat
49
52
  return false if @input.length <= 1
50
53
 
@@ -54,7 +54,7 @@ module Aikido::Zen
54
54
 
55
55
  def initialize(query, input, dialect)
56
56
  @query = query.downcase
57
- @input = input.downcase
57
+ @input = input.downcase.strip
58
58
  @dialect = dialect
59
59
  end
60
60
 
@@ -36,7 +36,7 @@ module Aikido::Zen
36
36
  def attack?
37
37
  return unless @config.stored_ssrf? # Feature flag
38
38
 
39
- return if @config.imds_allowed_hosts.include?(@hostname)
39
+ return if @config.imds_allowed_hosts.include?(@hostname.chomp("."))
40
40
 
41
41
  @addresses.find do |address|
42
42
  DANGEROUS_ADDRESSES.any? do |dangerous_address|
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Aikido
4
4
  module Zen
5
- VERSION = "1.4.1"
5
+ VERSION = "1.5.0"
6
6
 
7
7
  # The version of libzen_internals that we build against.
8
8
  LIBZEN_VERSION = "0.1.61"
data/lib/aikido/zen.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "zen/system_info"
10
10
  require_relative "zen/worker"
11
11
  require_relative "zen/agent"
12
12
  require_relative "zen/api_client"
13
+ require_relative "zen/api_stream"
13
14
  require_relative "zen/context"
14
15
  require_relative "zen/current_context"
15
16
  require_relative "zen/detached_agent"
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.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aikido Security
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-29 00:00:00.000000000 Z
11
+ date: 2026-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -98,6 +98,7 @@ files:
98
98
  - lib/aikido/zen/agent.rb
99
99
  - lib/aikido/zen/agent/heartbeats_manager.rb
100
100
  - lib/aikido/zen/api_client.rb
101
+ - lib/aikido/zen/api_stream.rb
101
102
  - lib/aikido/zen/attack.rb
102
103
  - lib/aikido/zen/attack_wave.rb
103
104
  - lib/aikido/zen/attack_wave/helpers.rb