aikido-zen 1.9.0.beta.4-aarch64-linux → 1.9.1-aarch64-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: c2035f9bc1d69cb7bc0cf1f85118b8173ee3f10fde479d0d69b50c1ce0b97c41
4
- data.tar.gz: 424db33452665750636979cbce63af815828b673a1f9443c6082e9943ca51f58
3
+ metadata.gz: d44cf3faf3ec9db53f802e4930f4d96bd6f84cc3857e18f308b890d464cfc9b5
4
+ data.tar.gz: 8d8a48ab88be06ea08164730196d41a690f5b27758a1f21c176e557ec67d6821
5
5
  SHA512:
6
- metadata.gz: e0e686f2263b046acd67d57ed3913284f2777679849fc9800c8aa5da1e277227134a5e209943b3bec7ebb8cbd5ee6ee270f3068b5cc3947eacc12934ec4041ca
7
- data.tar.gz: 1a0d32e49d58b6e14c02edcf32d0317bd428e67788e994f7ab5c10532ac8d5afb3dfb3656a06e6c5abe959ddac402dc3aec6fe41e5788ec9462c89d60ed6f6aa
6
+ metadata.gz: 86d0391a4cf47395fa06f21cd2d8a0626939c4a5cfc00be3aa383d2ed00534a2735be4c489d7c261f2182617e26b180932e12900748cdb2430ebe335b0766672
7
+ data.tar.gz: defb87e2ea7412e8b1b1abbce685f1d22650b928090a83afa7e65ee63fea4cc37700cf5561870e6851137b7fe7f5d3a7ac043e7f9f96c1f864bf23d1d6c20864
@@ -136,9 +136,7 @@ module Aikido::Zen
136
136
 
137
137
  # Recording the attack must never prevent us from blocking it.
138
138
  begin
139
- @config.logger.error(
140
- format("Zen has %s a %s: %s", attack.blocked? ? "blocked" : "detected", attack.humanized_name, attack.as_json.to_json)
141
- )
139
+ @config.logger.error(attack_log_message(attack))
142
140
 
143
141
  report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
144
142
 
@@ -227,6 +225,21 @@ module Aikido::Zen
227
225
 
228
226
  private
229
227
 
228
+ def attack_log_message(attack)
229
+ input = attack.as_json
230
+
231
+ format(
232
+ "Zen has %s a %s: %s(...) originating from %s%s",
233
+ attack.blocked? ? "blocked" : "detected",
234
+ attack.humanized_name,
235
+ attack.operation,
236
+ input["source"],
237
+ # The path is built from names in the request, so it can't be trusted
238
+ # not to split the log line.
239
+ input["path"].to_s.gsub(/[\r\n]/, " ")
240
+ )
241
+ end
242
+
230
243
  def settings_updated(event)
231
244
  updated_at = Time.at(event[:data]["configUpdatedAt"].to_i)
232
245
 
@@ -16,12 +16,6 @@ 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)
25
19
  end
26
20
 
27
21
  # @return [Boolean] whether we have a configured token.
@@ -44,16 +38,16 @@ module Aikido::Zen
44
38
 
45
39
  response = request(
46
40
  Net::HTTP::Get.new("/config", default_headers),
47
- base_url: @should_fetch_settings_endpoint
41
+ base_url: @config.realtime_endpoint,
42
+ timeout: @config.api_config_timeout
48
43
  )
49
44
 
50
45
  new_updated_at = Time.at(response["configUpdatedAt"].to_i)
51
46
  new_updated_at > last_updated_at
52
47
  end
53
48
 
54
- # Fetches the runtime config from the server. In case of a timeout or
55
- # other low-lever error, the request will be automatically retried up to two
56
- # times, after which it will raise an error.
49
+ # Fetches the runtime config from the server. Requests are never retried;
50
+ # on a timeout or other low-level error this raises an error.
57
51
  #
58
52
  # @return [Hash] decoded JSON response from the server with the runtime
59
53
  # settings.
@@ -61,7 +55,11 @@ module Aikido::Zen
61
55
  def fetch_runtime_config
62
56
  @config.logger.debug("Fetching new runtime config")
63
57
 
64
- request(Net::HTTP::Get.new("/api/runtime/config", default_headers))
58
+ request(
59
+ Net::HTTP::Get.new("/api/runtime/config", default_headers),
60
+ base_url: @config.api_endpoint,
61
+ timeout: @config.api_config_timeout
62
+ )
65
63
  end
66
64
 
67
65
  def fetch_runtime_firewall_lists
@@ -71,7 +69,11 @@ module Aikido::Zen
71
69
  "Accept-Encoding" => "gzip"
72
70
  })
73
71
 
74
- request(Net::HTTP::Get.new("/api/runtime/firewall/lists", headers))
72
+ request(
73
+ Net::HTTP::Get.new("/api/runtime/firewall/lists", headers),
74
+ base_url: @config.api_endpoint,
75
+ timeout: @config.api_config_timeout
76
+ )
75
77
  end
76
78
 
77
79
  # @overload report(event)
@@ -111,7 +113,7 @@ module Aikido::Zen
111
113
  @config.json_encoder.call(event)
112
114
  end
113
115
 
114
- request(req)
116
+ request(req, base_url: @config.api_endpoint, timeout: @config.api_events_timeout)
115
117
  rescue Aikido::Zen::RateLimitedError
116
118
  @rate_limiter.open!
117
119
  raise
@@ -121,15 +123,16 @@ module Aikido::Zen
121
123
  # response.
122
124
  #
123
125
  # @param request [Net::HTTPRequest]
124
- # @param base_url [URI] which API to use. Defaults to +Config#api_endpoint+.
126
+ # @param base_url [URI] which API to use.
127
+ # @param timeout [Numeric] seconds to wait on each connect, read and write.
125
128
  #
126
129
  # @return [Object] the result of decoding the JSON response from the server.
127
130
  #
128
131
  # @raise [Aikido::Zen::APIError] in case of a 4XX or 5XX response.
129
132
  # @raise [Aikido::Zen::NetworkError] if an error occurs trying to make the
130
133
  # request.
131
- private def request(request, base_url: @config.api_endpoint)
132
- Net::HTTP.start(base_url.host, base_url.port, http_settings(base_url)) do |http|
134
+ private def request(request, base_url:, timeout:)
135
+ Net::HTTP.start(base_url.host, base_url.port, http_settings(base_url, timeout)) do |http|
133
136
  response = http.request(request)
134
137
 
135
138
  case response
@@ -168,11 +171,14 @@ module Aikido::Zen
168
171
  end
169
172
  end
170
173
 
171
- private def http_settings(base_url)
172
- @http_settings ||= {
174
+ private def http_settings(base_url, timeout)
175
+ {
173
176
  use_ssl: base_url.scheme == "https",
174
- max_retries: 2
175
- }.merge(@config.api_timeouts)
177
+ max_retries: 0,
178
+ open_timeout: timeout,
179
+ read_timeout: timeout,
180
+ write_timeout: timeout
181
+ }
176
182
  end
177
183
 
178
184
  private def default_headers
@@ -180,7 +186,7 @@ module Aikido::Zen
180
186
  "Authorization" => @config.api_token,
181
187
  "Accept" => "application/json",
182
188
  "User-Agent" => "#{@system_info.library_name} v#{@system_info.library_version}"
183
- }
189
+ }.merge(@system_info.agent_headers)
184
190
  end
185
191
  end
186
192
  end
@@ -7,7 +7,9 @@ require "json"
7
7
  module Aikido::Zen
8
8
  class APIStream
9
9
  def initialize(
10
- config: Aikido::Zen.config,
10
+ zen: Aikido::Zen,
11
+ config: zen.config,
12
+ system_info: zen.system_info,
11
13
  min_backoff: 5,
12
14
  max_backoff: 60,
13
15
  backoff_reset: 30,
@@ -16,6 +18,7 @@ module Aikido::Zen
16
18
  read_timeout: 70
17
19
  )
18
20
  @config = config
21
+ @system_info = system_info
19
22
  @min_backoff = min_backoff
20
23
  @max_backoff = max_backoff
21
24
  @backoff_reset = backoff_reset
@@ -123,8 +126,7 @@ module Aikido::Zen
123
126
  request["Authorization"] = @token
124
127
  request["Accept"] = "text/event-stream"
125
128
  request["Cache-Control"] = "no-cache"
126
- request["X-Agent-Platform"] = "ruby"
127
- request["X-Agent-Version"] = Aikido::Zen::VERSION
129
+ @system_info.agent_headers.each { |name, value| request[name] = value }
128
130
  # Working around Net::HTTP cleverness; auto-negotiates gzip and buffers
129
131
  # decompressed output internally.
130
132
  request["Accept-Encoding"] = "identity"
@@ -43,13 +43,19 @@ module Aikido::Zen
43
43
  end
44
44
 
45
45
  def as_json
46
- {
46
+ attack_json = {
47
47
  "kind" => kind,
48
48
  "blocked" => blocked?,
49
49
  "metadata" => metadata,
50
50
  "operation" => @operation,
51
51
  "stack" => @stack
52
52
  }.compact.merge(input.as_json)
53
+
54
+ if (actor = @context&.request&.actor)
55
+ attack_json["user"] = {"id" => actor.id, "name" => actor.name}.compact
56
+ end
57
+
58
+ attack_json
53
59
  end
54
60
 
55
61
  def exception(*)
@@ -45,8 +45,15 @@ module Aikido::Zen
45
45
  # +https://guard.aikido.dev+.
46
46
  attr_reader :realtime_endpoint
47
47
 
48
- # @return [Hash] HTTP timeouts for communicating with the API.
49
- attr_reader :api_timeouts
48
+ # @return [Numeric] the timeout in seconds for reporting events to the
49
+ # API (+POST /api/runtime/events+). Applied to each connect, read and
50
+ # write wait. Defaults to 60 seconds.
51
+ attr_accessor :api_events_timeout
52
+
53
+ # @return [Numeric] the timeout in seconds for fetching config, runtime
54
+ # config and firewall lists from the API. Applied to each connect, read
55
+ # and write wait. Defaults to 30 seconds.
56
+ attr_accessor :api_config_timeout
50
57
 
51
58
  # @return [Integer] the interval in seconds to poll the runtime API for
52
59
  # settings changes. Defaults to evey 60 seconds.
@@ -254,7 +261,8 @@ module Aikido::Zen
254
261
  self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
255
262
  self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT") { regional_api_endpoint(api_token) }
256
263
  self.realtime_endpoint = api_endpoint
257
- self.api_timeouts = 10
264
+ self.api_events_timeout = 60 # sec
265
+ self.api_config_timeout = 30 # sec
258
266
  self.polling_interval = 60 # 1 min
259
267
  self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
260
268
  self.extract_payloads_max_depth = 64
@@ -332,25 +340,6 @@ module Aikido::Zen
332
340
  @logger.level = Logger::DEBUG if debugging
333
341
  end
334
342
 
335
- # @overload def api_timeouts=(timeouts)
336
- # Configure granular connection timeouts for the Aikido Zen API. You
337
- # can set any of these per call.
338
- # @param timeouts [Hash]
339
- # @option timeouts [Integer] :open_timeout Duration in seconds.
340
- # @option timeouts [Integer] :read_timeout Duration in seconds.
341
- # @option timeouts [Integer] :write_timeout Duration in seconds.
342
- #
343
- # @overload def api_timeouts=(duration)
344
- # Configure the connection timeouts for the Aikido Zen API.
345
- # @param duration [Integer] Duration in seconds to set for all three
346
- # timeouts (open, read, and write).
347
- def api_timeouts=(value)
348
- value = {open_timeout: value, read_timeout: value, write_timeout: value} if value.respond_to?(:to_int)
349
-
350
- @api_timeouts ||= {}
351
- @api_timeouts.update(value)
352
- end
353
-
354
343
  def api_token_hash
355
344
  return unless api_token
356
345
 
@@ -85,36 +85,40 @@ module Aikido::Zen
85
85
  # @raise [Aikido::UnderAttackError] if an attack is detected and
86
86
  # blocking_mode is enabled.
87
87
  def scan(context: Aikido::Zen.current_context, **scan_params)
88
+ # Nested scans must return early without clearing the scanning state.
88
89
  return if context&.scanning
90
+
89
91
  context&.scanning = true
90
92
 
91
- return if context&.protection_disabled?
93
+ begin
94
+ return if context&.protection_disabled?
92
95
 
93
- scan = Scan.new(sink: self, context: context)
96
+ scan = Scan.new(sink: self, context: context)
94
97
 
95
- scans_performed = 0
96
- scan.perform do
97
- result = nil
98
+ scans_performed = 0
99
+ scan.perform do
100
+ result = nil
98
101
 
99
- scanners.each do |scanner|
100
- next if scanner.skips_on_nil_context? && context.nil?
102
+ scanners.each do |scanner|
103
+ next if scanner.skips_on_nil_context? && context.nil?
101
104
 
102
- result = scanner.call(scan: scan, sink: self, context: context, **scan_params)
103
- scans_performed += 1
105
+ result = scanner.call(scan: scan, sink: self, context: context, **scan_params)
106
+ scans_performed += 1
104
107
 
105
- break result if result
106
- rescue => error
107
- scan.track_error(error, scanner)
108
- end
108
+ break result if result
109
+ rescue => error
110
+ scan.track_error(error, scanner)
111
+ end
109
112
 
110
- result
111
- end
113
+ result
114
+ end
112
115
 
113
- @reporter.call(scan) if scans_performed > 0
116
+ @reporter.call(scan) if scans_performed > 0
114
117
 
115
- scan
116
- ensure
117
- context&.scanning = false
118
+ scan
119
+ ensure
120
+ context&.scanning = false
121
+ end
118
122
  end
119
123
  end
120
124
  end
@@ -1,13 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "socket"
4
+ require "securerandom"
4
5
  require "rubygems"
5
6
 
6
7
  require_relative "package"
7
8
 
8
9
  module Aikido::Zen
9
- # Provides information about the currently running Agent.
10
+ # Provides information about the currently running agent.
10
11
  class SystemInfo
12
+ # @return [String] a unique identifier for the currently running agent.
13
+ attr_reader :session_id
14
+
15
+ def initialize
16
+ @session_id = generate_session_id
17
+ end
18
+
11
19
  def attacks_block_requests?
12
20
  !!Aikido::Zen.blocking_mode?
13
21
  end
@@ -28,8 +36,29 @@ module Aikido::Zen
28
36
  RUBY_VERSION
29
37
  end
30
38
 
39
+ # @return [String, nil] the hostname, or nil if it cannot be determined.
31
40
  def hostname
32
- @hostname ||= Socket.gethostname
41
+ @hostname ||= begin
42
+ name = Socket.gethostname.to_s
43
+ name unless name.empty?
44
+ rescue SystemCallError
45
+ nil
46
+ end
47
+ end
48
+
49
+ # Headers sent with every request to the Aikido server to describe and
50
+ # identify the currently running agent.
51
+ #
52
+ # @return [Hash{String => String}] the agent headers, keyed by header name.
53
+ def agent_headers
54
+ {
55
+ "X-Agent-Platform" => "ruby",
56
+ "X-Agent-Library" => library_name,
57
+ "X-Agent-Version" => library_version,
58
+ "X-Agent-Hostname" => hostname || "unknown",
59
+ "X-Agent-IP-Address" => ip_address || "unknown",
60
+ "X-Agent-Session-Id" => session_id
61
+ }
33
62
  end
34
63
 
35
64
  # @return [Array<Aikido::Zen::Package>] a list of loaded rubygems that are
@@ -41,14 +70,18 @@ module Aikido::Zen
41
70
  .select(&:supported?)
42
71
  end
43
72
 
44
- # @return [String] the first non-loopback IPv4 address that we can use
45
- # to identify this host. If the machine is solely identified by IPv6
46
- # addresses, then this will instead return an IPv6 address.
73
+ # Find the first non-loopback IP address, preferring IPv4.
74
+ #
75
+ # @return [String, nil] the IP address, or nil if it cannot be determined.
47
76
  def ip_address
48
- @ip_address ||= Socket.ip_address_list
49
- .reject { |ip| ip.ipv4_loopback? || ip.ipv6_loopback? || ip.unix? }
50
- .min_by { |ip| ip.ipv4? ? 0 : 1 }
51
- .ip_address
77
+ @ip_address ||= begin
78
+ address = Socket.ip_address_list
79
+ .reject { |ip| ip.ipv4_loopback? || ip.ipv6_loopback? || ip.unix? }
80
+ .min_by { |ip| ip.ipv4? ? 0 : 1 }
81
+ address&.ip_address
82
+ rescue SystemCallError
83
+ nil
84
+ end
52
85
  end
53
86
 
54
87
  def os_name
@@ -76,5 +109,17 @@ module Aikido::Zen
76
109
  "preventedPrototypePollution" => false
77
110
  }
78
111
  end
112
+
113
+ private def generate_session_id
114
+ # Code coverage is disabled here because `SecureRandom.uuid_v7` is only
115
+ # defined in Ruby 3.3+.
116
+ # :nocov:
117
+ if SecureRandom.respond_to?(:uuid_v7)
118
+ SecureRandom.uuid_v7
119
+ else
120
+ SecureRandom.uuid
121
+ end
122
+ # :nocov:
123
+ end
79
124
  end
80
125
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Aikido
4
4
  module Zen
5
- VERSION = "1.9.0.beta.4"
5
+ VERSION = "1.9.1"
6
6
 
7
7
  # The version of libzen_internals that we build against.
8
8
  LIBZEN_VERSION = "0.1.74"
data/lib/aikido/zen.rb CHANGED
@@ -560,6 +560,7 @@ module Aikido
560
560
  @agent = nil
561
561
  agent&.stop!
562
562
 
563
+ @system_info = nil
563
564
  @agent = Agent.start
564
565
  end
565
566
 
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.9.0.beta.4
4
+ version: 1.9.1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Aikido Security
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-09 00:00:00.000000000 Z
11
+ date: 2026-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby