aikido-zen 1.7.2.beta.2 → 1.8.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/docs/request-bypassing.md +50 -0
  3. data/lib/aikido/zen/agent.rb +15 -0
  4. data/lib/aikido/zen/collector/event.rb +12 -6
  5. data/lib/aikido/zen/collector/sink_stats.rb +4 -0
  6. data/lib/aikido/zen/collector/stats.rb +6 -2
  7. data/lib/aikido/zen/collector.rb +6 -6
  8. data/lib/aikido/zen/config.rb +8 -5
  9. data/lib/aikido/zen/context.rb +5 -0
  10. data/lib/aikido/zen/middleware/allowed_address_checker.rb +3 -2
  11. data/lib/aikido/zen/middleware/attack_protector.rb +1 -1
  12. data/lib/aikido/zen/middleware/attack_wave_protector.rb +2 -3
  13. data/lib/aikido/zen/middleware/ip_list_checker.rb +1 -5
  14. data/lib/aikido/zen/middleware/rack_throttler.rb +1 -1
  15. data/lib/aikido/zen/middleware/request_tracker.rb +8 -8
  16. data/lib/aikido/zen/middleware/user_agent_checker.rb +1 -5
  17. data/lib/aikido/zen/sink.rb +8 -3
  18. data/lib/aikido/zen/sinks/action_controller.rb +1 -1
  19. data/lib/aikido/zen/sinks/async_http.rb +3 -7
  20. data/lib/aikido/zen/sinks/curb.rb +3 -7
  21. data/lib/aikido/zen/sinks/em_http.rb +3 -7
  22. data/lib/aikido/zen/sinks/excon.rb +3 -7
  23. data/lib/aikido/zen/sinks/file.rb +1 -1
  24. data/lib/aikido/zen/sinks/http.rb +3 -7
  25. data/lib/aikido/zen/sinks/httpclient.rb +3 -7
  26. data/lib/aikido/zen/sinks/httpx.rb +3 -7
  27. data/lib/aikido/zen/sinks/kernel.rb +1 -1
  28. data/lib/aikido/zen/sinks/mysql2.rb +1 -1
  29. data/lib/aikido/zen/sinks/net_http.rb +3 -7
  30. data/lib/aikido/zen/sinks/patron.rb +3 -7
  31. data/lib/aikido/zen/sinks/pg.rb +1 -1
  32. data/lib/aikido/zen/sinks/resolv.rb +1 -1
  33. data/lib/aikido/zen/sinks/socket.rb +1 -1
  34. data/lib/aikido/zen/sinks/sqlite3.rb +1 -1
  35. data/lib/aikido/zen/sinks/trilogy.rb +1 -1
  36. data/lib/aikido/zen/sinks/typhoeus.rb +3 -7
  37. data/lib/aikido/zen/version.rb +1 -1
  38. data/lib/aikido/zen.rb +25 -0
  39. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aedfb64ca08c10194c0483138460f984e086ad2ac95250d3ce570b4c08b48263
4
- data.tar.gz: 6332c4155469c35483394a76683169e973feea9ba417fb4c4f434f8df3231b2e
3
+ metadata.gz: c01eda1d0ea486bf41d00d6efc05f064ad13b3ae71e112af02b1c01e3d164592
4
+ data.tar.gz: 222be5ac12985a6f855335a3433d32632704d6ebb1c0f7249fe9bb85492554f2
5
5
  SHA512:
6
- metadata.gz: 97a1b03ca7b5b92d944793c650f11791f7a36794e5ebcc85c6c41e6a93f71c1fbab3a5e32f0beeae6254c89ea06f23535275f20cc3732f18c8db722e4b6ec482
7
- data.tar.gz: 35ba0d4afe57aba7dac30c89294a875779fba9f36413c1c335d97efb62a47b5d3beaa8c5b96d31a9dda491d89b2dd0a78851a4d5ca83dd34071732c981d92b56
6
+ metadata.gz: 4645441dc4301bccc6c09347f864149b071f9cdcd16811309dd7a7e9650848ff90a559ebc576338e7bc8bc41fc3f4d4c2deaa096112628da02bf71619e69ad77
7
+ data.tar.gz: 8e7e3717a0a38dcb0d0c38abaf2142569404917f0fff5da720fcd4d180eebb27799d7b836a5222389a33a1a963e431f2459fe2299ab1d6e0c85234b615ce997d
@@ -0,0 +1,50 @@
1
+ # Bypass Zen for a specific request
2
+
3
+ Call `Aikido::Zen.request_bypassed!` from a Rack middleware to bypass Zen for this request. A bypassed request is fully excluded from Zen inspection and enforcement: Zen will not analyze the request, generate findings, or apply blocking rules for that traffic. Your application handles the request normally.
4
+
5
+ > [!NOTE]
6
+ > Zen's built-in [Bypassed IPs](https://help.aikido.dev/zen-firewall/zen-features/bypassed-ips) feature uses request bypassing internally, triggered by a matching IP/CIDR. `Aikido::Zen.request_bypassed!` lets you bypass requests using your custom logic.
7
+
8
+ ## What gets bypassed
9
+
10
+ * **Attack protection** — SQL injection, path traversal, command injection, and SSRF attacks are not blocked or reported
11
+ * **Rate limiting** — never triggered.
12
+ * **IP blocking** — Known Threat Actors, Tor traffic blocking/monitoring, country blocking, and custom IP allow/block lists are not checked.
13
+ * **Bot traffic blocking** — not checked.
14
+ * **User blocking** — blocked users are not blocked.
15
+ * **Statistics** — the request isn't counted, and doesn't count against your monitored request quota.
16
+ * **Attack wave protection** — the request doesn't count towards wave detection.
17
+
18
+ ## Usage
19
+
20
+ Insert your middleware directly after `Aikido::Zen::Middleware::ContextSetter`, so it runs after the request context is set up but before Zen's other middleware:
21
+
22
+ ```ruby
23
+ # config/application.rb
24
+ class RequestBypasser
25
+ def initialize(app)
26
+ @app = app
27
+ end
28
+
29
+ def call(env)
30
+ if your_custom_logic?(env)
31
+ Aikido::Zen.request_bypassed! # Bypasses Zen for this request
32
+ end
33
+
34
+ @app.call(env)
35
+ end
36
+ end
37
+
38
+ module YourApp
39
+ class Application < Rails::Application
40
+ # ...
41
+
42
+ initializer "your_app.insert_request_bypasser", after: "aikido.add_middleware" do |app|
43
+ app.middleware.insert_after Aikido::Zen::Middleware::ContextSetter, RequestBypasser
44
+ end
45
+ end
46
+ end
47
+ ```
48
+
49
+ > [!WARNING]
50
+ > A bypassed request gets zero protection from Zen — no attack detection, no rate limiting, no blocking, no tracking. Ensure that your custom logic only bypasses requests that you fully trust.
@@ -220,6 +220,8 @@ module Aikido::Zen
220
220
  updated_at = Time.at(event[:data]["configUpdatedAt"].to_i)
221
221
 
222
222
  if should_fetch_settings?(updated_at)
223
+ return if realtime_settings_updates_too_fast?
224
+
223
225
  if update_settings_from_runtime_config!(@api_client.fetch_runtime_config, reason: "after server-sent event")
224
226
  updated_settings!
225
227
 
@@ -239,6 +241,19 @@ module Aikido::Zen
239
241
  updated_at > last_updated_at
240
242
  end
241
243
 
244
+ def realtime_settings_updates_too_fast?
245
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
246
+
247
+ last_updated_at = @realtime_settings_updates_last_updated_at
248
+ if last_updated_at && now - last_updated_at < @config.realtime_settings_updates_min_time_between_events
249
+ return true
250
+ end
251
+
252
+ @realtime_settings_updates_last_updated_at = now
253
+
254
+ false
255
+ end
256
+
242
257
  def heartbeats
243
258
  @heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
244
259
  config: @config,
@@ -157,14 +157,16 @@ module Aikido::Zen
157
157
  def self.from_json(data)
158
158
  new(
159
159
  data["sink_name"],
160
+ data["sink_kind"],
160
161
  data["duration"],
161
162
  has_errors: data["has_errors"]
162
163
  )
163
164
  end
164
165
 
165
- def initialize(sink_name, duration, has_errors:)
166
+ def initialize(sink_name, sink_kind, duration, has_errors:)
166
167
  super()
167
168
  @sink_name = sink_name
169
+ @sink_kind = sink_kind
168
170
  @duration = duration
169
171
  @has_errors = has_errors
170
172
  end
@@ -172,17 +174,18 @@ module Aikido::Zen
172
174
  def as_json
173
175
  super.update({
174
176
  "sink_name" => @sink_name,
177
+ "sink_kind" => @sink_kind,
175
178
  "duration" => @duration,
176
179
  "has_errors" => @has_errors
177
180
  })
178
181
  end
179
182
 
180
183
  def handle(collector)
181
- collector.handle_track_scan(@sink_name, @duration, has_errors: @has_errors)
184
+ collector.handle_track_scan(@sink_name, @sink_kind, @duration, has_errors: @has_errors)
182
185
  end
183
186
 
184
187
  def inspect
185
- "#<#{self.class.name} #{@sink_name} #{format "%0.6f", @duration} #{@has_errors}>"
188
+ "#<#{self.class.name} #{@sink_name} #{@sink_kind} #{format "%0.6f", @duration} #{@has_errors}>"
186
189
  end
187
190
  end
188
191
 
@@ -192,29 +195,32 @@ module Aikido::Zen
192
195
  def self.from_json(data)
193
196
  new(
194
197
  data["sink_name"],
198
+ data["sink_kind"],
195
199
  being_blocked: data["being_blocked"]
196
200
  )
197
201
  end
198
202
 
199
- def initialize(sink_name, being_blocked:)
203
+ def initialize(sink_name, sink_kind, being_blocked:)
200
204
  super()
201
205
  @sink_name = sink_name
206
+ @sink_kind = sink_kind
202
207
  @being_blocked = being_blocked
203
208
  end
204
209
 
205
210
  def as_json
206
211
  super.update({
207
212
  "sink_name" => @sink_name,
213
+ "sink_kind" => @sink_kind,
208
214
  "being_blocked" => @being_blocked
209
215
  })
210
216
  end
211
217
 
212
218
  def handle(collector)
213
- collector.handle_track_attack(@sink_name, being_blocked: @being_blocked)
219
+ collector.handle_track_attack(@sink_name, @sink_kind, being_blocked: @being_blocked)
214
220
  end
215
221
 
216
222
  def inspect
217
- "#<#{self.class.name} #{@sink_name} #{@being_blocked}>"
223
+ "#<#{self.class.name} #{@sink_name} #{@sink_kind} #{@being_blocked}>"
218
224
  end
219
225
  end
220
226
 
@@ -7,6 +7,9 @@ module Aikido::Zen
7
7
  #
8
8
  # Tracks data specific to a single Sink.
9
9
  class Collector::SinkStats
10
+ # @return [String] category of operation this sink represents.
11
+ attr_accessor :kind
12
+
10
13
  # @return [Integer] number of total calls to Sink#scan.
11
14
  attr_accessor :scans
12
15
 
@@ -62,6 +65,7 @@ module Aikido::Zen
62
65
 
63
66
  def as_json
64
67
  {
68
+ "kind" => @kind,
65
69
  "total" => @scans,
66
70
  "interceptorThrewError" => @errors,
67
71
  "withoutContext" => 0,
@@ -94,21 +94,25 @@ module Aikido::Zen
94
94
  end
95
95
 
96
96
  # @param sink_name [String] the name of the sink
97
+ # @param sink_kind [String] the category of operation the sink represents
97
98
  # @param duration [Float] the length the scan in seconds
98
99
  # @param has_errors [Boolean] whether errors occurred during the scan
99
100
  # @return [void]
100
- def add_scan(sink_name, duration, has_errors:)
101
+ def add_scan(sink_name, sink_kind, duration, has_errors:)
101
102
  stats = @sinks[sink_name]
103
+ stats.kind = sink_kind
102
104
  stats.scans += 1
103
105
  stats.errors += 1 if has_errors
104
106
  stats.add_timing(duration)
105
107
  end
106
108
 
107
109
  # @param sink_name [String] the name of the sink
110
+ # @param sink_kind [String] the category of operation the sink represents
108
111
  # @param being_blocked [Boolean] whether the Agent blocked the request
109
112
  # @return [void]
110
- def add_attack(sink_name, being_blocked:)
113
+ def add_attack(sink_name, sink_kind, being_blocked:)
111
114
  stats = @sinks[sink_name]
115
+ stats.kind = sink_kind
112
116
  stats.attacks += 1
113
117
  stats.blocked_attacks += 1 if being_blocked
114
118
  end
@@ -146,12 +146,12 @@ module Aikido::Zen
146
146
  # @param scan [Aikido::Zen::Scan]
147
147
  # @return [void]
148
148
  def track_scan(scan)
149
- add_event(Events::TrackScan.new(scan.sink.name, scan.duration, has_errors: scan.errors?))
149
+ add_event(Events::TrackScan.new(scan.sink.name, scan.sink.kind, scan.duration, has_errors: scan.errors?))
150
150
  end
151
151
 
152
- def handle_track_scan(sink_name, duration, has_errors:)
152
+ def handle_track_scan(sink_name, sink_kind, duration, has_errors:)
153
153
  synchronize(@stats) do |stats|
154
- stats.add_scan(sink_name, duration, has_errors: has_errors)
154
+ stats.add_scan(sink_name, sink_kind, duration, has_errors: has_errors)
155
155
  end
156
156
  end
157
157
 
@@ -160,12 +160,12 @@ module Aikido::Zen
160
160
  # @param attack [Aikido::Zen::Attack]
161
161
  # @return [void]
162
162
  def track_attack(attack)
163
- add_event(Events::TrackAttack.new(attack.sink.name, being_blocked: attack.blocked?))
163
+ add_event(Events::TrackAttack.new(attack.sink.name, attack.sink.kind, being_blocked: attack.blocked?))
164
164
  end
165
165
 
166
- def handle_track_attack(sink_name, being_blocked:)
166
+ def handle_track_attack(sink_name, sink_kind, being_blocked:)
167
167
  synchronize(@stats) do |stats|
168
- stats.add_attack(sink_name, being_blocked: being_blocked)
168
+ stats.add_attack(sink_name, sink_kind, being_blocked: being_blocked)
169
169
  end
170
170
  end
171
171
 
@@ -42,7 +42,7 @@ module Aikido::Zen
42
42
  attr_reader :api_endpoint
43
43
 
44
44
  # @return [URI] The HTTP host for the Aikido Runtime API. Defaults to
45
- # +https://runtime.aikido.dev+.
45
+ # +https://guard.aikido.dev+.
46
46
  attr_reader :realtime_endpoint
47
47
 
48
48
  # @return [Hash] HTTP timeouts for communicating with the API.
@@ -229,6 +229,11 @@ module Aikido::Zen
229
229
  attr_accessor :realtime_settings_updates_enabled
230
230
  alias_method :realtime_settings_updates_enabled?, :realtime_settings_updates_enabled
231
231
 
232
+ # @return [Integer] the minimum time in milliseconds between realtime
233
+ # settings updates.
234
+ # Defaults to 9 seconds in milliseconds.
235
+ attr_accessor :realtime_settings_updates_min_time_between_events
236
+
232
237
  def initialize
233
238
  self.insert_middleware_after = ::ActionDispatch::RemoteIp
234
239
  self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
@@ -236,7 +241,7 @@ module Aikido::Zen
236
241
  self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
237
242
  self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
238
243
  self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT") { regional_api_endpoint(api_token) }
239
- self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
244
+ self.realtime_endpoint = api_endpoint
240
245
  self.api_timeouts = 10
241
246
  self.polling_interval = 60 # 1 min
242
247
  self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
@@ -277,6 +282,7 @@ module Aikido::Zen
277
282
  self.idor_excluded_table_names = []
278
283
  self.idor_max_cache_entries = 1000
279
284
  self.realtime_settings_updates_enabled = false
285
+ self.realtime_settings_updates_min_time_between_events = 9 * 1000 # 9 sec (ms)
280
286
  end
281
287
 
282
288
  # Set the base URL for API requests.
@@ -377,9 +383,6 @@ module Aikido::Zen
377
383
  "AU" => "https://guard.au.aikido.dev"
378
384
  }.freeze
379
385
 
380
- # @!visibility private
381
- DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
382
-
383
386
  # @!visibility private
384
387
  DEFAULT_JSON_ENCODER = JSON.method(:dump)
385
388
 
@@ -24,6 +24,10 @@ module Aikido::Zen
24
24
  attr_accessor :scanning
25
25
  alias_method :scanning?, :scanning
26
26
 
27
+ # @return [Boolean, nil] whether the request is bypassed.
28
+ attr_accessor :request_bypassed
29
+ alias_method :request_bypassed?, :request_bypassed
30
+
27
31
  # @return [Boolean] whether attack protection for the currently requested
28
32
  # endpoint was disabled on the Aikido dashboard, or if the source IP for
29
33
  # this request is in the "Bypass List".
@@ -48,6 +52,7 @@ module Aikido::Zen
48
52
 
49
53
  @metadata = {}
50
54
  @scanning = false
55
+ @request_bypassed = nil
51
56
  @protection_disabled = false
52
57
  @idor_protection_enabled = false
53
58
  end
@@ -5,8 +5,9 @@ module Aikido::Zen
5
5
  # Middleware that only allows allowed IPs when allowed IPs are configured for
6
6
  # any matching route in the Aikido dashboard.
7
7
  class AllowedAddressChecker
8
- def initialize(app, config: Aikido::Zen.config, settings: Aikido::Zen.runtime_settings)
8
+ def initialize(app, zen: Aikido::Zen, config: zen.config, settings: zen.runtime_settings)
9
9
  @app = app
10
+ @zen = zen
10
11
  @config = config
11
12
  @settings = settings
12
13
  end
@@ -22,7 +23,7 @@ module Aikido::Zen
22
23
  end
23
24
 
24
25
  private def allowed?(request)
25
- return true if @settings.bypassed_ips.include?(request.client_ip)
26
+ return true if @zen.request_bypassed?
26
27
 
27
28
  matches = @settings.endpoints.matched_settings(request.route)
28
29
 
@@ -19,7 +19,7 @@ module Aikido::Zen
19
19
  end
20
20
 
21
21
  private def protection_disabled?(request)
22
- return true if @settings.bypassed_ips.include?(request.client_ip)
22
+ return true if @zen.request_bypassed?
23
23
 
24
24
  !@settings.endpoints.matched_settings(request.route).all?(&:protected?)
25
25
  end
@@ -4,10 +4,9 @@ module Aikido
4
4
  module Zen
5
5
  module Middleware
6
6
  class AttackWaveProtector
7
- def initialize(app, zen: Aikido::Zen, settings: Aikido::Zen.runtime_settings)
7
+ def initialize(app, zen: Aikido::Zen)
8
8
  @app = app
9
9
  @zen = zen
10
- @settings = settings
11
10
  end
12
11
 
13
12
  def call(env)
@@ -30,7 +29,7 @@ module Aikido
30
29
  request = context.request
31
30
  return nil if request.nil?
32
31
 
33
- return nil if @settings.bypassed_ips.include?(request.client_ip)
32
+ return nil if @zen.request_bypassed?
34
33
 
35
34
  @zen.detect_attack_wave(context, status_code)
36
35
  end
@@ -15,7 +15,7 @@ module Aikido::Zen
15
15
 
16
16
  client_ip = request.client_ip
17
17
 
18
- return @app.call(env) if bypassed_ip?(client_ip)
18
+ return @app.call(env) if @zen.request_bypassed?
19
19
 
20
20
  if !@settings.allowed_ip?(client_ip)
21
21
  return @config.blocked_responder.call(request, :ip_allowed_list)
@@ -38,10 +38,6 @@ module Aikido::Zen
38
38
 
39
39
  @app.call(env)
40
40
  end
41
-
42
- def bypassed_ip?(client_ip)
43
- @settings.bypassed_ips.include?(client_ip)
44
- end
45
41
  end
46
42
  end
47
43
  end
@@ -34,7 +34,7 @@ module Aikido::Zen
34
34
  private
35
35
 
36
36
  def should_throttle?(request)
37
- return false if @settings.bypassed_ips.include?(request.client_ip)
37
+ return false if @zen.request_bypassed?
38
38
 
39
39
  return false if request.actor && @settings.user_excluded_from_rate_limiting?(request.actor.id)
40
40
 
@@ -5,8 +5,9 @@ module Aikido::Zen
5
5
  # Rack middleware used to track request
6
6
  # It implements the logic under that which is considered worthy of being tracked.
7
7
  class RequestTracker
8
- def initialize(app, settings: Aikido::Zen.runtime_settings)
8
+ def initialize(app, zen: Aikido::Zen, settings: zen.runtime_settings)
9
9
  @app = app
10
+ @zen = zen
10
11
  @settings = settings
11
12
  end
12
13
 
@@ -17,13 +18,12 @@ module Aikido::Zen
17
18
  if request.route && track?(
18
19
  status_code: response[0],
19
20
  route: request.route.path,
20
- http_method: request.request_method,
21
- ip: request.client_ip
21
+ http_method: request.request_method
22
22
  )
23
- Aikido::Zen.track_request(request)
23
+ @zen.track_request(request)
24
24
 
25
- if Aikido::Zen.config.collect_api_schema?
26
- Aikido::Zen.track_discovered_route(request)
25
+ if @zen.config.collect_api_schema?
26
+ @zen.track_discovered_route(request)
27
27
  end
28
28
  end
29
29
 
@@ -128,8 +128,8 @@ module Aikido::Zen
128
128
  # @param status_code [Integer]
129
129
  # @param route [String]
130
130
  # @param http_method [String]
131
- def track?(status_code:, route:, http_method:, ip: nil)
132
- return false if @settings.bypassed_ips.include?(ip)
131
+ def track?(status_code:, route:, http_method:)
132
+ return false if @zen.request_bypassed?
133
133
 
134
134
  # In the UI we want to show only successful (2xx) or redirect (3xx) responses
135
135
  # anything else is discarded.
@@ -13,7 +13,7 @@ module Aikido::Zen
13
13
  def call(env)
14
14
  request = Aikido::Zen::Middleware.request_from(env)
15
15
 
16
- return @app.call(env) if bypassed?(request)
16
+ return @app.call(env) if @zen.request_bypassed?
17
17
 
18
18
  user_agent = request.user_agent
19
19
 
@@ -31,10 +31,6 @@ module Aikido::Zen
31
31
 
32
32
  @app.call(env)
33
33
  end
34
-
35
- def bypassed?(request)
36
- @settings.bypassed_ips.include?(request.client_ip)
37
- end
38
34
  end
39
35
  end
40
36
  end
@@ -15,6 +15,7 @@ module Aikido::Zen
15
15
  # @param name [String] name of the library being patched. (This must
16
16
  # match the name of the gem, or we won't report that gem as
17
17
  # supported.)
18
+ # @param kind [String] category of operation this sink represents.
18
19
  # @param scanners [Array<#call>] a list of objects that respond to
19
20
  # #call with a Hash and return an Attack or nil.
20
21
  # @param opts [Hash<Symbol, Object>] any other options to pass to
@@ -23,9 +24,9 @@ module Aikido::Zen
23
24
  # @return [void]
24
25
  # @raise [ArgumentError] if a Sink with this name has already been
25
26
  # registered.
26
- def self.add(name, scanners:, **opts)
27
+ def self.add(name, kind, scanners:, **opts)
27
28
  raise ArgumentError, "Sink #{name} already registered" if registry.key?(name.to_s)
28
- registry[name.to_s] = Sink.new(name.to_s, scanners: scanners, **opts)
29
+ registry[name.to_s] = Sink.new(name.to_s, kind, scanners: scanners, **opts)
29
30
  end
30
31
  end
31
32
 
@@ -42,6 +43,9 @@ module Aikido::Zen
42
43
  # @return [String] name of the patched library (e.g. "mysql2").
43
44
  attr_reader :name
44
45
 
46
+ # @return [String] category of operation this sink represents.
47
+ attr_reader :kind
48
+
45
49
  # @return [Array<#call>] list of registered scanners for this sink.
46
50
  attr_reader :scanners
47
51
 
@@ -53,10 +57,11 @@ module Aikido::Zen
53
57
 
54
58
  DEFAULT_REPORTER = ->(scan) { Aikido::Zen.track_scan(scan) }
55
59
 
56
- def initialize(name, scanners:, operation: name, reporter: DEFAULT_REPORTER)
60
+ def initialize(name, kind, scanners:, operation: name, reporter: DEFAULT_REPORTER)
57
61
  raise ArgumentError, "scanners cannot be empty" if scanners.empty?
58
62
 
59
63
  @name = name
64
+ @kind = kind
60
65
  @operation = operation
61
66
  @scanners = scanners
62
67
  @reporter = reporter
@@ -30,7 +30,7 @@ module Aikido::Zen
30
30
 
31
31
  request = context.request
32
32
 
33
- return false if @settings.bypassed_ips.include?(request.client_ip)
33
+ return false if @zen.request_bypassed?
34
34
 
35
35
  if should_block_user?(request)
36
36
  status, headers, body = @config.blocked_responder.call(request, :user)
@@ -6,7 +6,7 @@ module Aikido::Zen
6
6
  module Sinks
7
7
  module Async
8
8
  module HTTP
9
- SINK = Sinks.add("async-http", scanners: [
9
+ SINK = Sinks.add("async-http", "outgoing_http_op", scanners: [
10
10
  Scanners::SSRFScanner
11
11
  ])
12
12
 
@@ -50,14 +50,10 @@ module Aikido::Zen
50
50
 
51
51
  connection = OutboundConnection.from_uri(uri)
52
52
 
53
- settings = Aikido::Zen.runtime_settings
54
-
55
- client_ip = context&.request&.client_ip
56
-
57
- unless settings.bypassed_ip?(client_ip)
53
+ unless Aikido::Zen.request_bypassed?
58
54
  Aikido::Zen.track_outbound(connection)
59
55
 
60
- if settings.block_outbound?(connection)
56
+ if Aikido::Zen.block_outbound?(connection)
61
57
  Sinks::DSL.presafe do
62
58
  raise OutboundConnectionBlockedError.new(connection)
63
59
  end
@@ -5,7 +5,7 @@ require_relative "../scanners/ssrf_scanner"
5
5
  module Aikido::Zen
6
6
  module Sinks
7
7
  module Curl
8
- SINK = Sinks.add("curb", scanners: [
8
+ SINK = Sinks.add("curb", "outgoing_http_op", scanners: [
9
9
  Scanners::SSRFScanner
10
10
  ])
11
11
 
@@ -62,14 +62,10 @@ module Aikido::Zen
62
62
 
63
63
  connection = OutboundConnection.from_uri(URI(url))
64
64
 
65
- settings = Aikido::Zen.runtime_settings
66
-
67
- client_ip = context&.request&.client_ip
68
-
69
- unless settings.bypassed_ip?(client_ip)
65
+ unless Aikido::Zen.request_bypassed?
70
66
  Aikido::Zen.track_outbound(connection)
71
67
 
72
- if settings.block_outbound?(connection)
68
+ if Aikido::Zen.block_outbound?(connection)
73
69
  Sinks::DSL.presafe do
74
70
  raise OutboundConnectionBlockedError.new(connection)
75
71
  end
@@ -6,7 +6,7 @@ module Aikido::Zen
6
6
  module Sinks
7
7
  module EventMachine
8
8
  module HttpRequest
9
- SINK = Sinks.add("em-http-request", scanners: [
9
+ SINK = Sinks.add("em-http-request", "outgoing_http_op", scanners: [
10
10
  Scanners::SSRFScanner
11
11
  ])
12
12
 
@@ -48,14 +48,10 @@ module Aikido::Zen
48
48
  port: req.port
49
49
  )
50
50
 
51
- settings = Aikido::Zen.runtime_settings
52
-
53
- client_ip = context&.request&.client_ip
54
-
55
- unless settings.bypassed_ip?(client_ip)
51
+ unless Aikido::Zen.request_bypassed?
56
52
  Aikido::Zen.track_outbound(connection)
57
53
 
58
- if settings.block_outbound?(connection)
54
+ if Aikido::Zen.block_outbound?(connection)
59
55
  Sinks::DSL.presafe do
60
56
  raise OutboundConnectionBlockedError.new(connection)
61
57
  end
@@ -5,7 +5,7 @@ require_relative "../scanners/ssrf_scanner"
5
5
  module Aikido::Zen
6
6
  module Sinks
7
7
  module Excon
8
- SINK = Sinks.add("excon", scanners: [
8
+ SINK = Sinks.add("excon", "outgoing_http_op", scanners: [
9
9
  Scanners::SSRFScanner
10
10
  ])
11
11
 
@@ -54,14 +54,10 @@ module Aikido::Zen
54
54
 
55
55
  connection = OutboundConnection.from_uri(request.uri)
56
56
 
57
- settings = Aikido::Zen.runtime_settings
58
-
59
- client_ip = context&.request&.client_ip
60
-
61
- unless settings.bypassed_ip?(client_ip)
57
+ unless Aikido::Zen.request_bypassed?
62
58
  Aikido::Zen.track_outbound(connection)
63
59
 
64
- if settings.block_outbound?(connection)
60
+ if Aikido::Zen.block_outbound?(connection)
65
61
  Sinks::DSL.presafe do
66
62
  raise OutboundConnectionBlockedError.new(connection)
67
63
  end
@@ -3,7 +3,7 @@
3
3
  module Aikido::Zen
4
4
  module Sinks
5
5
  module File
6
- SINK = Sinks.add("File", scanners: [Scanners::PathTraversalScanner])
6
+ SINK = Sinks.add("File", "fs_op", scanners: [Scanners::PathTraversalScanner])
7
7
 
8
8
  module Helpers
9
9
  def self.scan(filepath, operation)
@@ -5,7 +5,7 @@ require_relative "../scanners/ssrf_scanner"
5
5
  module Aikido::Zen
6
6
  module Sinks
7
7
  module HTTP
8
- SINK = Sinks.add("http", scanners: [
8
+ SINK = Sinks.add("http", "outgoing_http_op", scanners: [
9
9
  Scanners::SSRFScanner
10
10
  ])
11
11
 
@@ -68,14 +68,10 @@ module Aikido::Zen
68
68
 
69
69
  connection = Helpers.build_outbound(req)
70
70
 
71
- settings = Aikido::Zen.runtime_settings
72
-
73
- client_ip = context&.request&.client_ip
74
-
75
- unless settings.bypassed_ip?(client_ip)
71
+ unless Aikido::Zen.request_bypassed?
76
72
  Aikido::Zen.track_outbound(connection)
77
73
 
78
- if settings.block_outbound?(connection)
74
+ if Aikido::Zen.block_outbound?(connection)
79
75
  Sinks::DSL.presafe do
80
76
  raise OutboundConnectionBlockedError.new(connection)
81
77
  end
@@ -5,7 +5,7 @@ require_relative "../scanners/ssrf_scanner"
5
5
  module Aikido::Zen
6
6
  module Sinks
7
7
  module HTTPClient
8
- SINK = Sinks.add("httpclient", scanners: [
8
+ SINK = Sinks.add("httpclient", "outgoing_http_op", scanners: [
9
9
  Scanners::SSRFScanner
10
10
  ])
11
11
 
@@ -48,14 +48,10 @@ module Aikido::Zen
48
48
  context["ssrf.request"] = wrapped_request
49
49
  end
50
50
 
51
- settings = Aikido::Zen.runtime_settings
52
-
53
- client_ip = context&.request&.client_ip
54
-
55
- unless settings.bypassed_ip?(client_ip)
51
+ unless Aikido::Zen.request_bypassed?
56
52
  Aikido::Zen.track_outbound(connection)
57
53
 
58
- if settings.block_outbound?(connection)
54
+ if Aikido::Zen.block_outbound?(connection)
59
55
  Sinks::DSL.presafe do
60
56
  raise OutboundConnectionBlockedError.new(connection)
61
57
  end
@@ -5,7 +5,7 @@ require_relative "../scanners/ssrf_scanner"
5
5
  module Aikido::Zen
6
6
  module Sinks
7
7
  module HTTPX
8
- SINK = Sinks.add("httpx", scanners: [
8
+ SINK = Sinks.add("httpx", "outgoing_http_op", scanners: [
9
9
  Scanners::SSRFScanner
10
10
  ])
11
11
 
@@ -53,14 +53,10 @@ module Aikido::Zen
53
53
 
54
54
  connection = OutboundConnection.from_uri(request.uri)
55
55
 
56
- settings = Aikido::Zen.runtime_settings
57
-
58
- client_ip = context&.request&.client_ip
59
-
60
- unless settings.bypassed_ip?(client_ip)
56
+ unless Aikido::Zen.request_bypassed?
61
57
  Aikido::Zen.track_outbound(connection)
62
58
 
63
- if settings.block_outbound?(connection)
59
+ if Aikido::Zen.block_outbound?(connection)
64
60
  Sinks::DSL.presafe do
65
61
  raise OutboundConnectionBlockedError.new(connection)
66
62
  end
@@ -3,7 +3,7 @@
3
3
  module Aikido::Zen
4
4
  module Sinks
5
5
  module Kernel
6
- SINK = Sinks.add("Kernel", scanners: [Scanners::ShellInjectionScanner])
6
+ SINK = Sinks.add("Kernel", "exec_op", scanners: [Scanners::ShellInjectionScanner])
7
7
 
8
8
  module Helpers
9
9
  def self.scan(command, operation)
@@ -3,7 +3,7 @@
3
3
  module Aikido::Zen
4
4
  module Sinks
5
5
  module Mysql2
6
- SINK = Sinks.add("mysql2", scanners: [Scanners::SQLInjectionScanner])
6
+ SINK = Sinks.add("mysql2", "sql_op", scanners: [Scanners::SQLInjectionScanner])
7
7
 
8
8
  module Helpers
9
9
  def self.scan(query, operation)
@@ -6,7 +6,7 @@ module Aikido::Zen
6
6
  module Sinks
7
7
  module Net
8
8
  module HTTP
9
- SINK = Sinks.add("net-http", scanners: [
9
+ SINK = Sinks.add("net-http", "outgoing_http_op", scanners: [
10
10
  Scanners::SSRFScanner
11
11
  ])
12
12
 
@@ -84,14 +84,10 @@ module Aikido::Zen
84
84
 
85
85
  connection = Helpers.build_outbound(self)
86
86
 
87
- settings = Aikido::Zen.runtime_settings
88
-
89
- client_ip = context&.request&.client_ip
90
-
91
- unless settings.bypassed_ip?(client_ip)
87
+ unless Aikido::Zen.request_bypassed?
92
88
  Aikido::Zen.track_outbound(connection)
93
89
 
94
- if settings.block_outbound?(connection)
90
+ if Aikido::Zen.block_outbound?(connection)
95
91
  Sinks::DSL.presafe do
96
92
  raise OutboundConnectionBlockedError.new(connection)
97
93
  end
@@ -5,7 +5,7 @@ require_relative "../scanners/ssrf_scanner"
5
5
  module Aikido::Zen
6
6
  module Sinks
7
7
  module Patron
8
- SINK = Sinks.add("patron", scanners: [
8
+ SINK = Sinks.add("patron", "outgoing_http_op", scanners: [
9
9
  Scanners::SSRFScanner
10
10
  ])
11
11
 
@@ -57,14 +57,10 @@ module Aikido::Zen
57
57
 
58
58
  connection = OutboundConnection.from_uri(URI(request.url))
59
59
 
60
- settings = Aikido::Zen.runtime_settings
61
-
62
- client_ip = context&.request&.client_ip
63
-
64
- unless settings.bypassed_ip?(client_ip)
60
+ unless Aikido::Zen.request_bypassed?
65
61
  Aikido::Zen.track_outbound(connection)
66
62
 
67
- if settings.block_outbound?(connection)
63
+ if Aikido::Zen.block_outbound?(connection)
68
64
  Sinks::DSL.presafe do
69
65
  raise OutboundConnectionBlockedError.new(connection)
70
66
  end
@@ -3,7 +3,7 @@
3
3
  module Aikido::Zen
4
4
  module Sinks
5
5
  module PG
6
- SINK = Sinks.add("pg", scanners: [Scanners::SQLInjectionScanner])
6
+ SINK = Sinks.add("pg", "sql_op", scanners: [Scanners::SQLInjectionScanner])
7
7
 
8
8
  module Helpers
9
9
  # For some reason, the ActiveRecord pg adaptor does not wrap exceptions
@@ -6,7 +6,7 @@ require_relative "../scanners/ssrf_scanner"
6
6
  module Aikido::Zen
7
7
  module Sinks
8
8
  module Resolv
9
- SINK = Sinks.add("resolv", scanners: [
9
+ SINK = Sinks.add("resolv", "outgoing_http_op", scanners: [
10
10
  Scanners::StoredSSRFScanner,
11
11
  Scanners::SSRFScanner
12
12
  ])
@@ -10,7 +10,7 @@ module Aikido::Zen
10
10
  # there's no way to access the internal DNS resolution that happens in C
11
11
  # when using the socket primitives.
12
12
  module Socket
13
- SINK = Sinks.add("socket", scanners: [
13
+ SINK = Sinks.add("socket", "outgoing_http_op", scanners: [
14
14
  Scanners::StoredSSRFScanner,
15
15
  Scanners::SSRFScanner
16
16
  ])
@@ -3,7 +3,7 @@
3
3
  module Aikido::Zen
4
4
  module Sinks
5
5
  module SQLite3
6
- SINK = Sinks.add("sqlite3", scanners: [Scanners::SQLInjectionScanner])
6
+ SINK = Sinks.add("sqlite3", "sql_op", scanners: [Scanners::SQLInjectionScanner])
7
7
 
8
8
  module Helpers
9
9
  def self.scan(query, operation)
@@ -3,7 +3,7 @@
3
3
  module Aikido::Zen
4
4
  module Sinks
5
5
  module Trilogy
6
- SINK = Sinks.add("trilogy", scanners: [Scanners::SQLInjectionScanner])
6
+ SINK = Sinks.add("trilogy", "sql_op", scanners: [Scanners::SQLInjectionScanner])
7
7
 
8
8
  module Helpers
9
9
  def self.scan(query, operation)
@@ -5,7 +5,7 @@ require_relative "../sink"
5
5
  module Aikido::Zen
6
6
  module Sinks
7
7
  module Typhoeus
8
- SINK = Sinks.add("typhoeus", scanners: [
8
+ SINK = Sinks.add("typhoeus", "outgoing_http_op", scanners: [
9
9
  Aikido::Zen::Scanners::SSRFScanner
10
10
  ])
11
11
 
@@ -24,14 +24,10 @@ module Aikido::Zen
24
24
 
25
25
  connection = Aikido::Zen::OutboundConnection.from_uri(URI(request.base_url))
26
26
 
27
- settings = Aikido::Zen.runtime_settings
28
-
29
- client_ip = context&.request&.client_ip
30
-
31
- unless settings.bypassed_ip?(client_ip)
27
+ unless Aikido::Zen.request_bypassed?
32
28
  Aikido::Zen.track_outbound(connection)
33
29
 
34
- if settings.block_outbound?(connection)
30
+ if Aikido::Zen.block_outbound?(connection)
35
31
  raise OutboundConnectionBlockedError.new(connection)
36
32
  end
37
33
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Aikido
4
4
  module Zen
5
- VERSION = "1.7.2.beta.2"
5
+ VERSION = "1.8.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
@@ -153,6 +153,23 @@ module Aikido
153
153
  Fiber.current.aikido_current_context = context
154
154
  end
155
155
 
156
+ # @return [Boolean] whether the current request is bypassed.
157
+ def self.request_bypassed?
158
+ context = current_context
159
+ return unless context
160
+
161
+ return context.request_bypassed unless context.request_bypassed.nil?
162
+
163
+ context.request_bypassed = runtime_settings.bypassed_ip?(context.request.client_ip)
164
+ end
165
+
166
+ # Marks the current request as bypassed.
167
+ #
168
+ # @return [void]
169
+ def self.request_bypassed!
170
+ current_context&.request_bypassed = true
171
+ end
172
+
156
173
  # Track statistics about an HTTP request the app is handling.
157
174
  #
158
175
  # @param request [Aikido::Zen::Request]
@@ -207,6 +224,14 @@ module Aikido
207
224
  collector.track_outbound(connection)
208
225
  end
209
226
 
227
+ # @param connection [Aikido::Zen::OutboundConnection]
228
+ # @return [Boolean] whether this outbound connection should be blocked.
229
+ def self.block_outbound?(connection)
230
+ return false if current_context&.protection_disabled?
231
+
232
+ runtime_settings.block_outbound?(connection)
233
+ end
234
+
210
235
  # Track statistics about the result of a Sink's scan, and report it as
211
236
  # an Attack if one is detected.
212
237
  #
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.7.2.beta.2
4
+ version: 1.8.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-08-14 00:00:00.000000000 Z
11
+ date: 2026-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -93,6 +93,7 @@ files:
93
93
  - docs/invalid-sql-queries.md
94
94
  - docs/proxy.md
95
95
  - docs/rails.md
96
+ - docs/request-bypassing.md
96
97
  - docs/troubleshooting.md
97
98
  - lib/aikido-zen.rb
98
99
  - lib/aikido/zen.rb