aikido-zen 1.9.0.beta.3-arm64-linux → 1.9.1-arm64-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/docs/rails.md +14 -0
- data/lib/aikido/zen/agent.rb +25 -5
- data/lib/aikido/zen/api_client.rb +27 -21
- data/lib/aikido/zen/api_stream.rb +5 -3
- data/lib/aikido/zen/attack.rb +16 -4
- data/lib/aikido/zen/config.rb +28 -23
- data/lib/aikido/zen/context.rb +32 -10
- data/lib/aikido/zen/helpers.rb +16 -0
- data/lib/aikido/zen/internals.rb +3 -11
- data/lib/aikido/zen/payload.rb +5 -2
- data/lib/aikido/zen/request.rb +12 -3
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +2 -2
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +2 -2
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +2 -2
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +8 -3
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +14 -1
- data/lib/aikido/zen/sink.rb +23 -19
- data/lib/aikido/zen/system_info.rb +54 -9
- data/lib/aikido/zen/version.rb +1 -1
- data/lib/aikido/zen/worker_process/agent/client.rb +4 -1
- data/lib/aikido/zen/worker_process/agent/server.rb +5 -5
- data/lib/aikido/zen.rb +39 -0
- 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: df231e175ddfcbc1a8c21d8c0e23575d13de65a47214a0c191b9b94bdc7d7b51
|
|
4
|
+
data.tar.gz: 257407c5c9630af6ea66536e4ffc2ae9e8042b5eba98eb53f5baa61bf04d1ba8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d758d8a41ab1387576a0202135df2526f62aaa0d594104cce44812051e3941b24edcc7d06d86a9837d8542eab521c08f5d72513b687619c2609183f403a70b3f
|
|
7
|
+
data.tar.gz: 23997d7f758c63551deff8cefb2d067fa6c0816744f247f6ab15a89d7554aa6f9bfef0e8420a81ec4c66ad1043b7b6702990a6402cb64aed01063f0061478aae
|
data/docs/rails.md
CHANGED
|
@@ -75,6 +75,20 @@ class ApplicationController < ActionController::Base
|
|
|
75
75
|
end
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
### Rate limiting groups
|
|
79
|
+
|
|
80
|
+
To limit the number of requests for a group of users, you can use the
|
|
81
|
+
`Aikido::Zen.set_rate_limit_group` method. For example, this is useful if you
|
|
82
|
+
want to limit the number of requests per team or company.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
Aikido::Zen.set_rate_limit_group(current_user.team_id)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> [!NOTE]
|
|
89
|
+
> If a rate limiting group is set, the configured rate limits are only applied
|
|
90
|
+
> to the group and not to individual users or IP addresses.
|
|
91
|
+
|
|
78
92
|
## Configuration
|
|
79
93
|
|
|
80
94
|
Zen exposes its configuration object to the Rails configuration, which you can
|
data/lib/aikido/zen/agent.rb
CHANGED
|
@@ -134,12 +134,17 @@ module Aikido::Zen
|
|
|
134
134
|
def handle_attack(attack)
|
|
135
135
|
attack.will_be_blocked! if Aikido::Zen.blocking_mode?
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
# Recording the attack must never prevent us from blocking it.
|
|
138
|
+
begin
|
|
139
|
+
@config.logger.error(attack_log_message(attack))
|
|
140
|
+
|
|
141
|
+
report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
|
|
142
|
+
|
|
143
|
+
@collector.track_attack(attack)
|
|
144
|
+
rescue => err
|
|
145
|
+
@config.logger.error("Zen failed to record an attack: #{err.class}: #{err.message}")
|
|
146
|
+
end
|
|
141
147
|
|
|
142
|
-
@collector.track_attack(attack)
|
|
143
148
|
raise attack if attack.blocked?
|
|
144
149
|
end
|
|
145
150
|
|
|
@@ -220,6 +225,21 @@ module Aikido::Zen
|
|
|
220
225
|
|
|
221
226
|
private
|
|
222
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
|
+
|
|
223
243
|
def settings_updated(event)
|
|
224
244
|
updated_at = Time.at(event[:data]["configUpdatedAt"].to_i)
|
|
225
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: @
|
|
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.
|
|
55
|
-
# other low-
|
|
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(
|
|
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(
|
|
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.
|
|
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:
|
|
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
|
-
|
|
174
|
+
private def http_settings(base_url, timeout)
|
|
175
|
+
{
|
|
173
176
|
use_ssl: base_url.scheme == "https",
|
|
174
|
-
max_retries:
|
|
175
|
-
|
|
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
|
-
|
|
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[
|
|
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"
|
data/lib/aikido/zen/attack.rb
CHANGED
|
@@ -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(*)
|
|
@@ -69,8 +75,10 @@ module Aikido::Zen
|
|
|
69
75
|
end
|
|
70
76
|
|
|
71
77
|
def metadata
|
|
78
|
+
safe_filepath = Aikido::Zen::Helpers.encode_safely(filepath)
|
|
79
|
+
|
|
72
80
|
{
|
|
73
|
-
"filename" =>
|
|
81
|
+
"filename" => safe_filepath
|
|
74
82
|
}
|
|
75
83
|
end
|
|
76
84
|
|
|
@@ -106,8 +114,10 @@ module Aikido::Zen
|
|
|
106
114
|
end
|
|
107
115
|
|
|
108
116
|
def metadata
|
|
117
|
+
safe_command = Aikido::Zen::Helpers.encode_safely(@command)
|
|
118
|
+
|
|
109
119
|
{
|
|
110
|
-
"command" =>
|
|
120
|
+
"command" => safe_command
|
|
111
121
|
}
|
|
112
122
|
end
|
|
113
123
|
|
|
@@ -138,8 +148,10 @@ module Aikido::Zen
|
|
|
138
148
|
end
|
|
139
149
|
|
|
140
150
|
def metadata
|
|
151
|
+
safe_query = Aikido::Zen::Helpers.encode_safely(@query)
|
|
152
|
+
|
|
141
153
|
{
|
|
142
|
-
"sql" =>
|
|
154
|
+
"sql" => safe_query,
|
|
143
155
|
"dialect" => @dialect.name,
|
|
144
156
|
"failedToTokenize" => @failed_to_tokenize || nil
|
|
145
157
|
}.compact
|
data/lib/aikido/zen/config.rb
CHANGED
|
@@ -45,8 +45,15 @@ module Aikido::Zen
|
|
|
45
45
|
# +https://guard.aikido.dev+.
|
|
46
46
|
attr_reader :realtime_endpoint
|
|
47
47
|
|
|
48
|
-
# @return [
|
|
49
|
-
|
|
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.
|
|
@@ -56,6 +63,10 @@ module Aikido::Zen
|
|
|
56
63
|
# each initial heartbeat event.
|
|
57
64
|
attr_accessor :initial_heartbeat_delays
|
|
58
65
|
|
|
66
|
+
# @return [Integer] the maximum number of levels deep we want to traverse
|
|
67
|
+
# when extracting payloads from a nested request property. Defaults to 64.
|
|
68
|
+
attr_accessor :extract_payloads_max_depth
|
|
69
|
+
|
|
59
70
|
# @return [Symbol] the agent mode for forked worker processes. Can be set
|
|
60
71
|
# through the AIKIDO_AGENT_MODE environment variable.
|
|
61
72
|
attr_reader :agent_mode
|
|
@@ -107,6 +118,10 @@ module Aikido::Zen
|
|
|
107
118
|
# the oldest seen users.
|
|
108
119
|
attr_accessor :max_users_tracked
|
|
109
120
|
|
|
121
|
+
# @return [Integer] the maximum number of custom events tracked per request.
|
|
122
|
+
# Defaults to 25.
|
|
123
|
+
attr_accessor :max_custom_events
|
|
124
|
+
|
|
110
125
|
# @return [Proc{(Aikido::Zen::Request, Symbol, reason: String=nil) => Array(Integer, Hash, #each)}]
|
|
111
126
|
# Rack handler used to respond to requests from IPs, users or others blocked in the Aikido
|
|
112
127
|
# dashboard.
|
|
@@ -246,9 +261,11 @@ module Aikido::Zen
|
|
|
246
261
|
self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
|
|
247
262
|
self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT") { regional_api_endpoint(api_token) }
|
|
248
263
|
self.realtime_endpoint = api_endpoint
|
|
249
|
-
self.
|
|
264
|
+
self.api_events_timeout = 60 # sec
|
|
265
|
+
self.api_config_timeout = 30 # sec
|
|
250
266
|
self.polling_interval = 60 # 1 min
|
|
251
267
|
self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
|
|
268
|
+
self.extract_payloads_max_depth = 64
|
|
252
269
|
self.agent_mode = ENV.fetch("AIKIDO_AGENT_MODE", "shared")
|
|
253
270
|
self.worker_process_polling_interval = 10
|
|
254
271
|
self.worker_process_polling_jitter = 10
|
|
@@ -261,6 +278,7 @@ module Aikido::Zen
|
|
|
261
278
|
self.max_compressed_stats = 100
|
|
262
279
|
self.max_outbound_connections = 200
|
|
263
280
|
self.max_users_tracked = 1000
|
|
281
|
+
self.max_custom_events = 25
|
|
264
282
|
self.blocked_responder = DEFAULT_BLOCKED_RESPONDER
|
|
265
283
|
self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
|
|
266
284
|
self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
|
|
@@ -322,25 +340,6 @@ module Aikido::Zen
|
|
|
322
340
|
@logger.level = Logger::DEBUG if debugging
|
|
323
341
|
end
|
|
324
342
|
|
|
325
|
-
# @overload def api_timeouts=(timeouts)
|
|
326
|
-
# Configure granular connection timeouts for the Aikido Zen API. You
|
|
327
|
-
# can set any of these per call.
|
|
328
|
-
# @param timeouts [Hash]
|
|
329
|
-
# @option timeouts [Integer] :open_timeout Duration in seconds.
|
|
330
|
-
# @option timeouts [Integer] :read_timeout Duration in seconds.
|
|
331
|
-
# @option timeouts [Integer] :write_timeout Duration in seconds.
|
|
332
|
-
#
|
|
333
|
-
# @overload def api_timeouts=(duration)
|
|
334
|
-
# Configure the connection timeouts for the Aikido Zen API.
|
|
335
|
-
# @param duration [Integer] Duration in seconds to set for all three
|
|
336
|
-
# timeouts (open, read, and write).
|
|
337
|
-
def api_timeouts=(value)
|
|
338
|
-
value = {open_timeout: value, read_timeout: value, write_timeout: value} if value.respond_to?(:to_int)
|
|
339
|
-
|
|
340
|
-
@api_timeouts ||= {}
|
|
341
|
-
@api_timeouts.update(value)
|
|
342
|
-
end
|
|
343
|
-
|
|
344
343
|
def api_token_hash
|
|
345
344
|
return unless api_token
|
|
346
345
|
|
|
@@ -442,7 +441,13 @@ module Aikido::Zen
|
|
|
442
441
|
|
|
443
442
|
# @!visibility private
|
|
444
443
|
DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
|
|
445
|
-
|
|
444
|
+
rate_limiting_group = request.rate_limiting_group
|
|
445
|
+
return "group:#{rate_limiting_group}" unless rate_limiting_group.nil?
|
|
446
|
+
|
|
447
|
+
actor = request.actor
|
|
448
|
+
return "actor:#{request.actor.id}" unless actor.nil?
|
|
449
|
+
|
|
450
|
+
request.client_ip
|
|
446
451
|
}
|
|
447
452
|
end
|
|
448
453
|
end
|
data/lib/aikido/zen/context.rb
CHANGED
|
@@ -38,6 +38,9 @@ module Aikido::Zen
|
|
|
38
38
|
attr_accessor :idor_protection_enabled
|
|
39
39
|
alias_method :idor_protection_enabled?, :idor_protection_enabled
|
|
40
40
|
|
|
41
|
+
# @return [Integer]
|
|
42
|
+
attr_accessor :custom_events
|
|
43
|
+
|
|
41
44
|
# @param request [Rack::Request] a Request object that implements the
|
|
42
45
|
# Rack::Request API, to which we will delegate behavior.
|
|
43
46
|
# @param settings [Aikido::Zen::RuntimeSettings]
|
|
@@ -45,16 +48,20 @@ module Aikido::Zen
|
|
|
45
48
|
# @yieldparam request [Rack::Request] the given request object.
|
|
46
49
|
# @yieldreturn [Hash<Symbol, #flat_map>] map of payload source types
|
|
47
50
|
# to the actual data from the request to populate them.
|
|
48
|
-
def initialize(request,
|
|
51
|
+
def initialize(request, zen: Aikido::Zen, config: zen.config, settings: zen.runtime_settings, &sources)
|
|
49
52
|
@request = request
|
|
53
|
+
@config = config
|
|
50
54
|
@settings = settings
|
|
51
55
|
@payload_sources = sources
|
|
52
56
|
|
|
57
|
+
@max_depth = @config.extract_payloads_max_depth
|
|
58
|
+
|
|
53
59
|
@metadata = {}
|
|
54
60
|
@scanning = false
|
|
55
61
|
@request_bypassed = nil
|
|
56
62
|
@protection_disabled = false
|
|
57
63
|
@idor_protection_enabled = false
|
|
64
|
+
@custom_events = 0
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
# Fetch some metadata stored in the Context.
|
|
@@ -102,31 +109,46 @@ module Aikido::Zen
|
|
|
102
109
|
private
|
|
103
110
|
|
|
104
111
|
# @!visibility private
|
|
105
|
-
def extract_payloads_from(data, source_type, prefix = nil)
|
|
112
|
+
def extract_payloads_from(data, source_type, prefix = nil, depth = 0)
|
|
113
|
+
return [] if depth > @max_depth
|
|
114
|
+
|
|
106
115
|
if data.is_a?(String)
|
|
107
116
|
[Payload.new(data, source_type, prefix.to_s)]
|
|
108
117
|
elsif data.respond_to?(:to_hash)
|
|
109
118
|
data.to_hash.flat_map do |key, value|
|
|
110
|
-
extract_payloads_from(value, source_type, [prefix, key].compact.join("."))
|
|
119
|
+
extract_payloads_from(value, source_type, [prefix, key].compact.join("."), depth + 1)
|
|
111
120
|
end
|
|
112
121
|
elsif data.respond_to?(:to_ary)
|
|
113
122
|
array = data.to_ary
|
|
114
123
|
return array if array.empty?
|
|
115
124
|
|
|
116
125
|
payloads = array.flat_map.with_index do |value, index|
|
|
117
|
-
extract_payloads_from(value, source_type, [prefix, index].compact.join("."))
|
|
126
|
+
extract_payloads_from(value, source_type, [prefix, index].compact.join("."), depth + 1)
|
|
118
127
|
end
|
|
119
128
|
|
|
120
129
|
unless Aikido::Zen.config.harden?
|
|
121
130
|
# Special case for File.join given a possibly nested array of strings,
|
|
122
131
|
# as might occur when a query parameter is an array.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
|
|
133
|
+
# File.join recursively joins nested string arrays, and can overflow
|
|
134
|
+
# the stack given deeply nested arrays.
|
|
135
|
+
#
|
|
136
|
+
# Flatten array to max depth and check that the flattened array is an
|
|
137
|
+
# array of strings before calling File.join__internal_for_aikido_zen,
|
|
138
|
+
# only if the array was fully flattened, to prevent a stack overflow.
|
|
139
|
+
#
|
|
140
|
+
# Checking that all values are Strings handily prevents a TypeError
|
|
141
|
+
# from being raised.
|
|
142
|
+
flattened_array = array.flatten(@max_depth)
|
|
143
|
+
if flattened_array.all? { |string| string.is_a?(String) }
|
|
144
|
+
begin
|
|
145
|
+
string = File.join__internal_for_aikido_zen(*flattened_array)
|
|
146
|
+
if unsafe_path?(string)
|
|
147
|
+
payloads << Payload.new(string, source_type, [prefix, "__File.join__"].compact.join("."))
|
|
148
|
+
end
|
|
149
|
+
rescue
|
|
150
|
+
# Could not create special payload for File.join.
|
|
127
151
|
end
|
|
128
|
-
rescue
|
|
129
|
-
# Could not create special payload for File.join.
|
|
130
152
|
end
|
|
131
153
|
end
|
|
132
154
|
|
data/lib/aikido/zen/helpers.rb
CHANGED
|
@@ -29,6 +29,22 @@ module Aikido
|
|
|
29
29
|
|
|
30
30
|
Regexp.new(regexp.source, regexp.options, timeout: timeout)
|
|
31
31
|
end
|
|
32
|
+
|
|
33
|
+
# Returns a copy of the string safely encoded as UTF-8.
|
|
34
|
+
#
|
|
35
|
+
# Web servers may deliver request data as binary strings. These strings
|
|
36
|
+
# are relabeled as UTF-8 instead of being converted, because conversion
|
|
37
|
+
# would replace every non-ASCII byte with U+FFFD, even in valid UTF-8.
|
|
38
|
+
#
|
|
39
|
+
# @param string [String] the string
|
|
40
|
+
# @return [String] the valid UTF-8 string
|
|
41
|
+
def self.encode_safely(string)
|
|
42
|
+
if string.encoding == Encoding::BINARY
|
|
43
|
+
string.dup.force_encoding(Encoding::UTF_8).scrub
|
|
44
|
+
else
|
|
45
|
+
string.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
32
48
|
end
|
|
33
49
|
end
|
|
34
50
|
end
|
data/lib/aikido/zen/internals.rb
CHANGED
|
@@ -103,8 +103,8 @@ module Aikido::Zen
|
|
|
103
103
|
# @raise [Aikido::Zen::InternalsError] if there's a problem loading or
|
|
104
104
|
# calling libzen.
|
|
105
105
|
def self.detect_sql_injection(query, input, dialect)
|
|
106
|
-
query_bytes = encode_safely(query)
|
|
107
|
-
input_bytes = encode_safely(input)
|
|
106
|
+
query_bytes = Aikido::Zen::Helpers.encode_safely(query)
|
|
107
|
+
input_bytes = Aikido::Zen::Helpers.encode_safely(input)
|
|
108
108
|
|
|
109
109
|
query_ptr = FFI::MemoryPointer.new(:uint8, query_bytes.bytesize)
|
|
110
110
|
input_ptr = FFI::MemoryPointer.new(:uint8, input_bytes.bytesize)
|
|
@@ -123,7 +123,7 @@ module Aikido::Zen
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
def self.idor_analyze_sql(query, dialect)
|
|
126
|
-
query_bytes = encode_safely(query)
|
|
126
|
+
query_bytes = Aikido::Zen::Helpers.encode_safely(query)
|
|
127
127
|
query_ptr = FFI::MemoryPointer.new(:uint8, query_bytes.bytesize)
|
|
128
128
|
query_ptr.put_bytes(0, query_bytes)
|
|
129
129
|
|
|
@@ -134,13 +134,5 @@ module Aikido::Zen
|
|
|
134
134
|
JSON.parse(result_json)
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
|
-
|
|
138
|
-
class << self
|
|
139
|
-
private
|
|
140
|
-
|
|
141
|
-
def encode_safely(string)
|
|
142
|
-
string.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
137
|
end
|
|
146
138
|
end
|
data/lib/aikido/zen/payload.rb
CHANGED
|
@@ -24,10 +24,13 @@ module Aikido::Zen
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def as_json
|
|
27
|
+
safe_value = Aikido::Zen::Helpers.encode_safely(value.to_s)
|
|
28
|
+
safe_path = Aikido::Zen::Helpers.encode_safely(path.to_s)
|
|
29
|
+
|
|
27
30
|
{
|
|
28
|
-
"payload" =>
|
|
31
|
+
"payload" => safe_value,
|
|
29
32
|
"source" => SOURCE_SERIALIZATIONS[source],
|
|
30
|
-
"path" => ".#{
|
|
33
|
+
"path" => ".#{safe_path}"
|
|
31
34
|
}
|
|
32
35
|
end
|
|
33
36
|
|
data/lib/aikido/zen/request.rb
CHANGED
|
@@ -17,6 +17,11 @@ module Aikido::Zen
|
|
|
17
17
|
# @see Aikido::Zen.track_user
|
|
18
18
|
attr_accessor :actor
|
|
19
19
|
|
|
20
|
+
# The current rate limiting group, if set by the host app.
|
|
21
|
+
#
|
|
22
|
+
# @return [String, nil]
|
|
23
|
+
attr_accessor :rate_limiting_group
|
|
24
|
+
|
|
20
25
|
# The current tenant, if set by the host app.
|
|
21
26
|
#
|
|
22
27
|
# @return [Integer, String, nil]
|
|
@@ -86,13 +91,17 @@ module Aikido::Zen
|
|
|
86
91
|
end
|
|
87
92
|
|
|
88
93
|
def as_json
|
|
94
|
+
safe_url = Aikido::Zen::Helpers.encode_safely(url)
|
|
95
|
+
safe_user_agent = Aikido::Zen::Helpers.encode_safely(user_agent) if user_agent
|
|
96
|
+
safe_route_path = Aikido::Zen::Helpers.encode_safely(route.path) if route&.path
|
|
97
|
+
|
|
89
98
|
{
|
|
90
99
|
"method" => request_method.upcase,
|
|
91
|
-
"url" =>
|
|
100
|
+
"url" => safe_url,
|
|
92
101
|
"ipAddress" => client_ip,
|
|
93
|
-
"userAgent" =>
|
|
102
|
+
"userAgent" => safe_user_agent,
|
|
94
103
|
"source" => framework,
|
|
95
|
-
"route" =>
|
|
104
|
+
"route" => safe_route_path
|
|
96
105
|
}
|
|
97
106
|
end
|
|
98
107
|
|
|
@@ -40,8 +40,8 @@ module Aikido::Zen
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def initialize(filepath, input)
|
|
43
|
-
@filepath = filepath.downcase
|
|
44
|
-
@input = input.downcase
|
|
43
|
+
@filepath = Aikido::Zen::Helpers.encode_safely(filepath).downcase
|
|
44
|
+
@input = Aikido::Zen::Helpers.encode_safely(input).downcase
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def attack?
|
|
@@ -37,8 +37,8 @@ module Aikido::Zen
|
|
|
37
37
|
# @param command [String]
|
|
38
38
|
# @param input [String]
|
|
39
39
|
def initialize(command, input)
|
|
40
|
-
@command = command
|
|
41
|
-
@input = input
|
|
40
|
+
@command = Aikido::Zen::Helpers.encode_safely(command)
|
|
41
|
+
@input = Aikido::Zen::Helpers.encode_safely(input)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def attack?
|
|
@@ -53,8 +53,8 @@ module Aikido::Zen
|
|
|
53
53
|
attr_reader :failed_to_tokenize
|
|
54
54
|
|
|
55
55
|
def initialize(query, input, dialect)
|
|
56
|
-
@query = query.downcase
|
|
57
|
-
@input = input.downcase.strip
|
|
56
|
+
@query = Aikido::Zen::Helpers.encode_safely(query).downcase
|
|
57
|
+
@input = Aikido::Zen::Helpers.encode_safely(input).downcase.strip
|
|
58
58
|
@dialect = dialect
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -85,9 +85,11 @@ module Aikido::Zen
|
|
|
85
85
|
# Matches strings that contain only characters that appear in some
|
|
86
86
|
# form of IP address.
|
|
87
87
|
#
|
|
88
|
+
# An IPv6 zone ID (e.g. "%eth0") may follow the address.
|
|
89
|
+
#
|
|
88
90
|
# Hostnames are not expected to match, allowing `parse_address` to
|
|
89
91
|
# skip the `Socket.getaddrinfo` call.
|
|
90
|
-
ADDRESS_REGEXP = /\A[0-9a-fx:.]
|
|
92
|
+
ADDRESS_REGEXP = /\A[0-9a-fx:.]+(?:%[^%\s]+)?\z/i
|
|
91
93
|
|
|
92
94
|
# Parses `address` as an IP address, in any form that is accepted
|
|
93
95
|
# by `getaddrinfo`:
|
|
@@ -96,6 +98,7 @@ module Aikido::Zen
|
|
|
96
98
|
# * Plain integer IPv4 notation, in decimal, octal, or hexadecimal
|
|
97
99
|
# (e.g. "2130706433", "017700000001", "0x7f000001")
|
|
98
100
|
# * Shorthand dotted IPv4 notation (e.g. "127.1")
|
|
101
|
+
# * IPv6 notation with zone ID (e.g. "fe80::1%eth0")
|
|
99
102
|
#
|
|
100
103
|
# Delegates to `Socket.getaddrinfo` with the `AI_NUMERICHOST` flag,
|
|
101
104
|
# which never performs a DNS lookup.
|
|
@@ -105,8 +108,10 @@ module Aikido::Zen
|
|
|
105
108
|
def parse_address(address)
|
|
106
109
|
return nil unless address.is_a?(String) && ADDRESS_REGEXP.match?(address)
|
|
107
110
|
|
|
108
|
-
Socket.getaddrinfo(address, nil, :UNSPEC, :STREAM, nil, Socket::AI_NUMERICHOST)
|
|
109
|
-
.
|
|
111
|
+
Socket.getaddrinfo(address, nil, :UNSPEC, :STREAM, nil, Socket::AI_NUMERICHOST).map do |info|
|
|
112
|
+
# `IPAddr.new` only accepts a zone ID on Ruby 3.1+.
|
|
113
|
+
IPAddr.new(info[3].partition("%").first)
|
|
114
|
+
end
|
|
110
115
|
rescue SocketError
|
|
111
116
|
nil
|
|
112
117
|
end
|
|
@@ -146,7 +146,9 @@ module Aikido::Zen
|
|
|
146
146
|
#
|
|
147
147
|
# @return [Array<URI>] a list of unique URIs based on the above criteria.
|
|
148
148
|
def uris_from_input
|
|
149
|
-
input
|
|
149
|
+
# Percent-encode input so URI() can parse it, as an app may do before
|
|
150
|
+
# making the request.
|
|
151
|
+
input = percent_encode(@input.to_s)
|
|
150
152
|
|
|
151
153
|
# If you build a URI manually and set the hostname to an IPv6 string,
|
|
152
154
|
# the URI library will be helpful to wrap it in brackets so it's a
|
|
@@ -167,6 +169,17 @@ module Aikido::Zen
|
|
|
167
169
|
nil
|
|
168
170
|
end
|
|
169
171
|
|
|
172
|
+
# Matches bytes not allowed in a URI (RFC 3986): any byte other than
|
|
173
|
+
# the unreserved and reserved characters, and any "%" that does not
|
|
174
|
+
# start a percent-encoded byte.
|
|
175
|
+
NOT_ALLOWED_IN_URI = /%(?![0-9A-Fa-f]{2})|[^A-Za-z0-9\-._~:\/?#\[\]@!$&'()*+,;=%]/n
|
|
176
|
+
|
|
177
|
+
# Percent-encodes characters not allowed in a URI, preserving existing
|
|
178
|
+
# percent-encoding.
|
|
179
|
+
def percent_encode(string)
|
|
180
|
+
string.b.gsub(NOT_ALLOWED_IN_URI) { |byte| format("%%%02X", byte.ord) }
|
|
181
|
+
end
|
|
182
|
+
|
|
170
183
|
# Check if the input is an IPv6 that is not surrounded by square brackets.
|
|
171
184
|
def unescaped_ipv6?(input)
|
|
172
185
|
(
|
data/lib/aikido/zen/sink.rb
CHANGED
|
@@ -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
|
-
|
|
93
|
+
begin
|
|
94
|
+
return if context&.protection_disabled?
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
scan = Scan.new(sink: self, context: context)
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
scans_performed = 0
|
|
99
|
+
scan.perform do
|
|
100
|
+
result = nil
|
|
98
101
|
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
scanners.each do |scanner|
|
|
103
|
+
next if scanner.skips_on_nil_context? && context.nil?
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
result = scanner.call(scan: scan, sink: self, context: context, **scan_params)
|
|
106
|
+
scans_performed += 1
|
|
104
107
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
break result if result
|
|
109
|
+
rescue => error
|
|
110
|
+
scan.track_error(error, scanner)
|
|
111
|
+
end
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
result
|
|
114
|
+
end
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
@reporter.call(scan) if scans_performed > 0
|
|
114
117
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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 ||=
|
|
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
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
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 ||=
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
data/lib/aikido/zen/version.rb
CHANGED
|
@@ -64,7 +64,10 @@ module Aikido::Zen::WorkerProcess
|
|
|
64
64
|
def calculate_rate_limits(request)
|
|
65
65
|
result = @rpc_client.invoke(
|
|
66
66
|
"calculate_rate_limits", Aikido::Zen::IPC::READ_TIMEOUT,
|
|
67
|
-
request.route.as_json,
|
|
67
|
+
request.route.as_json,
|
|
68
|
+
request.client_ip,
|
|
69
|
+
request.actor&.as_json,
|
|
70
|
+
request.rate_limiting_group
|
|
68
71
|
)
|
|
69
72
|
|
|
70
73
|
Aikido::Zen::RateLimiter::Result.from_json(result) if result
|
|
@@ -22,8 +22,8 @@ module Aikido::Zen::WorkerProcess
|
|
|
22
22
|
send_collector_events(events_data)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
@rpc_server.handle("calculate_rate_limits") do |respond, route_data, ip, actor_data|
|
|
26
|
-
result = calculate_rate_limits(route_data, ip, actor_data)
|
|
25
|
+
@rpc_server.handle("calculate_rate_limits") do |respond, route_data, ip, actor_data, rate_limiting_group|
|
|
26
|
+
result = calculate_rate_limits(route_data, ip, actor_data, rate_limiting_group)
|
|
27
27
|
respond.call(result&.as_json)
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -66,7 +66,7 @@ module Aikido::Zen::WorkerProcess
|
|
|
66
66
|
|
|
67
67
|
# @api private
|
|
68
68
|
# @note Visible for testing.
|
|
69
|
-
RequestKind = Struct.new(:route, :schema, :client_ip, :actor)
|
|
69
|
+
RequestKind = Struct.new(:route, :schema, :client_ip, :actor, :rate_limiting_group)
|
|
70
70
|
|
|
71
71
|
def updated_settings(known_config_generation = nil, known_firewall_lists_generation = nil)
|
|
72
72
|
result = {}
|
|
@@ -91,10 +91,10 @@ module Aikido::Zen::WorkerProcess
|
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
def calculate_rate_limits(route_data, ip, actor_data)
|
|
94
|
+
def calculate_rate_limits(route_data, ip, actor_data, rate_limiting_group)
|
|
95
95
|
actor = Aikido::Zen::Actor.from_json(actor_data) if actor_data
|
|
96
96
|
route = Aikido::Zen::Route.from_json(route_data)
|
|
97
|
-
Aikido::Zen.rate_limiter.calculate_rate_limits(RequestKind.new(route, nil, ip, actor))
|
|
97
|
+
Aikido::Zen.rate_limiter.calculate_rate_limits(RequestKind.new(route, nil, ip, actor, rate_limiting_group))
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
# @return [Array<Hash>, nil]
|
data/lib/aikido/zen.rb
CHANGED
|
@@ -278,6 +278,37 @@ module Aikido
|
|
|
278
278
|
alias_method :set_user, :track_user
|
|
279
279
|
end
|
|
280
280
|
|
|
281
|
+
# Set the rate limiting group.
|
|
282
|
+
#
|
|
283
|
+
# @param group [String, Integer] the rate limiting group id
|
|
284
|
+
# @return [void]
|
|
285
|
+
def self.set_rate_limiting_group(group)
|
|
286
|
+
return if config.disabled?
|
|
287
|
+
|
|
288
|
+
if group.nil? || group == ""
|
|
289
|
+
config.logger.warn("set_rate_limiting_group expects a non-empty group")
|
|
290
|
+
return
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
unless group.is_a?(String) || group.is_a?(Integer)
|
|
294
|
+
config.logger.warn("set_rate_limiting_group expects a String or Integer group, got #{group.class}")
|
|
295
|
+
return
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
context = current_context
|
|
299
|
+
if context.nil?
|
|
300
|
+
config.logger.warn("set_rate_limiting_group was called without a context")
|
|
301
|
+
return
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
context.request.rate_limiting_group = group.to_s
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Align with other Zen implementations, while keeping internal consistency.
|
|
308
|
+
class << self
|
|
309
|
+
alias_method :set_rate_limit_group, :set_rate_limiting_group
|
|
310
|
+
end
|
|
311
|
+
|
|
281
312
|
# Track a custom event happening in your application, like a failed login or
|
|
282
313
|
# a password reset request. Zen automatically attaches the IP address, user
|
|
283
314
|
# agent, and current user (if you called .track_user) from the request.
|
|
@@ -300,6 +331,13 @@ module Aikido
|
|
|
300
331
|
|
|
301
332
|
return if request_bypassed?
|
|
302
333
|
|
|
334
|
+
if context.custom_events >= config.max_custom_events
|
|
335
|
+
config.logger.warn("Dropped custom event #{name}: too many custom events were tracked for this request")
|
|
336
|
+
return
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
context.custom_events += 1
|
|
340
|
+
|
|
303
341
|
event = Events::Custom.new(
|
|
304
342
|
name: name,
|
|
305
343
|
request: context.request,
|
|
@@ -522,6 +560,7 @@ module Aikido
|
|
|
522
560
|
@agent = nil
|
|
523
561
|
agent&.stop!
|
|
524
562
|
|
|
563
|
+
@system_info = nil
|
|
525
564
|
@agent = Agent.start
|
|
526
565
|
end
|
|
527
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.
|
|
4
|
+
version: 1.9.1
|
|
5
5
|
platform: arm64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Aikido Security
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|