aikido-zen 1.1.1 → 1.2.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/docs/rails.md +1 -1
  3. data/lib/aikido/zen/actor.rb +2 -2
  4. data/lib/aikido/zen/agent.rb +3 -0
  5. data/lib/aikido/zen/attack_wave/helpers.rb +1 -1
  6. data/lib/aikido/zen/attack_wave.rb +69 -7
  7. data/lib/aikido/zen/cache.rb +8 -2
  8. data/lib/aikido/zen/collector/event.rb +44 -1
  9. data/lib/aikido/zen/collector/stats.rb +18 -1
  10. data/lib/aikido/zen/collector.rb +27 -2
  11. data/lib/aikido/zen/config.rb +19 -6
  12. data/lib/aikido/zen/current_context.rb +27 -0
  13. data/lib/aikido/zen/detached_agent/agent.rb +1 -1
  14. data/lib/aikido/zen/detached_agent/front_object.rb +1 -1
  15. data/lib/aikido/zen/errors.rb +6 -0
  16. data/lib/aikido/zen/event.rb +0 -17
  17. data/lib/aikido/zen/middleware/allowed_address_checker.rb +12 -4
  18. data/lib/aikido/zen/middleware/attack_protector.rb +2 -3
  19. data/lib/aikido/zen/middleware/attack_wave_protector.rb +21 -3
  20. data/lib/aikido/zen/middleware/ip_list_checker.rb +47 -0
  21. data/lib/aikido/zen/middleware/rack_throttler.rb +6 -2
  22. data/lib/aikido/zen/middleware/request_tracker.rb +2 -3
  23. data/lib/aikido/zen/middleware/user_agent_checker.rb +1 -2
  24. data/lib/aikido/zen/outbound_connection.rb +1 -1
  25. data/lib/aikido/zen/rails_engine.rb +1 -0
  26. data/lib/aikido/zen/rate_limiter/bucket.rb +21 -3
  27. data/lib/aikido/zen/rate_limiter.rb +22 -15
  28. data/lib/aikido/zen/request/schema/auth_schemas.rb +8 -7
  29. data/lib/aikido/zen/request/schema/definition.rb +32 -2
  30. data/lib/aikido/zen/request/schema.rb +23 -7
  31. data/lib/aikido/zen/runtime_settings/domain_settings.rb +27 -0
  32. data/lib/aikido/zen/runtime_settings/domains.rb +41 -0
  33. data/lib/aikido/zen/runtime_settings/endpoints.rb +65 -14
  34. data/lib/aikido/zen/runtime_settings/ip_list.rb +34 -0
  35. data/lib/aikido/zen/runtime_settings/ip_set.rb +21 -1
  36. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +11 -0
  37. data/lib/aikido/zen/runtime_settings.rb +92 -7
  38. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +17 -4
  39. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +2 -2
  40. data/lib/aikido/zen/sinks/action_controller.rb +6 -2
  41. data/lib/aikido/zen/sinks/async_http.rb +15 -3
  42. data/lib/aikido/zen/sinks/curb.rb +15 -3
  43. data/lib/aikido/zen/sinks/em_http.rb +15 -3
  44. data/lib/aikido/zen/sinks/excon.rb +15 -3
  45. data/lib/aikido/zen/sinks/http.rb +15 -3
  46. data/lib/aikido/zen/sinks/httpclient.rb +15 -3
  47. data/lib/aikido/zen/sinks/httpx.rb +15 -3
  48. data/lib/aikido/zen/sinks/net_http.rb +15 -3
  49. data/lib/aikido/zen/sinks/patron.rb +15 -3
  50. data/lib/aikido/zen/sinks/typhoeus.rb +19 -5
  51. data/lib/aikido/zen/sinks.rb +5 -0
  52. data/lib/aikido/zen/version.rb +2 -2
  53. data/lib/aikido/zen.rb +20 -6
  54. metadata +21 -3
  55. data/lib/aikido/zen/outbound_connection_monitor.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff56705f799c52755fffdd6a584643ac02d8a4b275671fd488721fb14845a95a
4
- data.tar.gz: d7be9020470fa9ea1bdbaf2ea5e4624a2f208aa6fba8ef325cad70e916c74cef
3
+ metadata.gz: c6c3172c2e8ea724f5b362592c7c9d326396c2ff05ba98a1573ed7ed587a1e55
4
+ data.tar.gz: 7c63642333e1f64c6b3011a12f52637438b98348e94754da47cb72f3c84f10ec
5
5
  SHA512:
6
- metadata.gz: 9315eb15bb3c47f905566e857a7abad0ac8c575a1f11d6e0c357f1a0eccb0eda285000e45787cc6c3b48a3f126c291c08a16933e1a2d2b6ca47e05ad05241f0a
7
- data.tar.gz: 470adc0df77eb9026a4aa7a830a5b5102405ddc346d3d7a50d33975e1a5805ad8fe840ec8d15638bf083936cf0ea14cee1bb30b112045249e34341184ef9e0da
6
+ metadata.gz: 32e2a25c9f2f55a21a3bf2d6e487b36ec3b8780dcaae3db9a82324669a99bf8c0514a6bbe9c810c20b877039e0827fa2446c053f610cf1317b3d5f064bc1e653
7
+ data.tar.gz: 41dda121db561eea1d99b0d4c76df3672e1d6e210e9d841e48ea467418120b3ce665151dc7dae9d889593300c40da1738bf3fa2266084fd7af603882146d21e3
data/docs/rails.md CHANGED
@@ -6,7 +6,7 @@ To install Zen, add the gem:
6
6
  bundle add aikido-zen
7
7
  ```
8
8
 
9
- And require it before `Bundler.require` in `config/application.rb`:
9
+ And be sure to **require it before** `Bundler.require` in `config/application.rb`:
10
10
 
11
11
  ```ruby
12
12
  # config/application.rb
@@ -65,7 +65,7 @@ module Aikido::Zen
65
65
  def initialize(
66
66
  id:,
67
67
  name: nil,
68
- ip: Aikido::Zen.current_context&.request&.ip,
68
+ ip: Aikido::Zen.current_context&.request&.client_ip,
69
69
  first_seen_at: Time.now.utc,
70
70
  last_seen_at: first_seen_at
71
71
  )
@@ -96,7 +96,7 @@ module Aikido::Zen
96
96
  # always keep the most recent time if this conflicts with the current
97
97
  # value.
98
98
  # @return [void]
99
- def update(seen_at: Time.now.utc, ip: Aikido::Zen.current_context&.request&.ip)
99
+ def update(seen_at: Time.now.utc, ip: Aikido::Zen.current_context&.request&.client_ip)
100
100
  @last_seen_at.try_update { |last_seen_at| [last_seen_at, seen_at].max }
101
101
  @ip.try_update { |last_ip| [ip, last_ip].compact.first }
102
102
  end
@@ -160,6 +160,9 @@ module Aikido::Zen
160
160
  if Aikido::Zen.runtime_settings.update_from_runtime_config_json(response)
161
161
  updated_settings!
162
162
  @config.logger.info("Updated runtime settings after heartbeat")
163
+
164
+ Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(@api_client.fetch_runtime_firewall_lists)
165
+ @config.logger.info("Updated runtime firewall list after heartbeat")
163
166
  end
164
167
  end
165
168
  end
@@ -80,7 +80,7 @@ module Aikido::Zen
80
80
  ".dbus",
81
81
  ".docker",
82
82
  ".drush",
83
- ".TODO: gem",
83
+ ".gem",
84
84
  ".git",
85
85
  ".github",
86
86
  ".gnupg",
@@ -6,12 +6,19 @@ require_relative "attack_wave/helpers"
6
6
  module Aikido::Zen
7
7
  module AttackWave
8
8
  class Detector
9
+ # @return [Aikido::Zen::CappedSet]
10
+ attr_reader :samples
11
+
9
12
  def initialize(config: Aikido::Zen.config, clock: nil)
10
13
  @config = config
11
14
 
12
15
  @event_times = Cache.new(@config.attack_wave_max_cache_entries, ttl: @config.attack_wave_min_time_between_events, clock: clock)
13
16
 
14
17
  @request_counts = Cache.new(@config.attack_wave_max_cache_entries, 0, ttl: @config.attack_wave_min_time_between_requests, clock: clock)
18
+
19
+ @samples = Cache.new(@config.attack_wave_max_cache_entries, ttl: @config.attack_wave_min_time_between_requests, clock: clock) do
20
+ CappedSet.new(@config.attack_wave_max_cache_samples)
21
+ end
15
22
  end
16
23
 
17
24
  def attack_wave?(context)
@@ -25,6 +32,13 @@ module Aikido::Zen
25
32
 
26
33
  request_count = @request_counts[client_ip] += 1
27
34
 
35
+ context.request.then do |request|
36
+ @samples[client_ip] <<= Sample.new(
37
+ verb: request.request_method,
38
+ path: request.fullpath
39
+ )
40
+ end
41
+
28
42
  return false if request_count < @config.attack_wave_threshold
29
43
 
30
44
  @event_times[client_ip] = Time.now.utc
@@ -60,29 +74,77 @@ module Aikido::Zen
60
74
  source: @source
61
75
  }.compact
62
76
  end
77
+
78
+ def ==(other)
79
+ other.is_a?(self.class) &&
80
+ other.ip_address == ip_address &&
81
+ other.user_agent == user_agent &&
82
+ other.source == source
83
+ end
84
+ alias_method :eql?, :==
63
85
  end
64
86
 
65
87
  class Attack
66
- # @return [Hash<String, String>]
67
- attr_reader :metadata
88
+ # @return [Aikido::Zen::AttackWave::Sample]
89
+ attr_reader :samples
68
90
 
69
91
  # @return [Aikido::Zen::Actor]
70
92
  attr_reader :user
71
93
 
72
- # @param metadata [Hash<String, String>]
73
- # @param metadata [Aikido::Zen::Actor]
94
+ # @param samples [Aikido::Zen::AttackWave::Sample]
95
+ # @param user [Aikido::Zen::Actor]
74
96
  # @return [Aikido::Zen::AttackWave::Attack]
75
- def initialize(metadata:, user:)
76
- @metadata = metadata
97
+ def initialize(samples:, user:)
98
+ @samples = samples
77
99
  @user = user
78
100
  end
79
101
 
80
102
  def as_json
81
103
  {
82
- metadata: @metadata.as_json,
104
+ metadata: {
105
+ samples: @samples.as_json.to_json # The API only accepts string values in metadata
106
+ },
83
107
  user: @user.as_json
84
108
  }.compact
85
109
  end
110
+
111
+ def ==(other)
112
+ other.is_a?(self.class) &&
113
+ other.samples == samples &&
114
+ other.user == user
115
+ end
116
+ alias_method :eql?, :==
117
+ end
118
+
119
+ class Sample
120
+ # @return [String]
121
+ attr_reader :verb
122
+
123
+ # @return [String]
124
+ attr_reader :path
125
+
126
+ def initialize(verb:, path:)
127
+ @verb = verb
128
+ @path = path
129
+ end
130
+
131
+ def as_json
132
+ {
133
+ method: @verb.as_json,
134
+ url: @path.as_json
135
+ }.compact
136
+ end
137
+
138
+ def ==(other)
139
+ other.is_a?(self.class) &&
140
+ other.verb == verb &&
141
+ other.path == path
142
+ end
143
+ alias_method :eql?, :==
144
+
145
+ def hash
146
+ [verb, path].hash
147
+ end
86
148
  end
87
149
  end
88
150
  end
@@ -9,11 +9,13 @@ module Aikido::Zen
9
9
  def_delegators :@data,
10
10
  :size, :empty?
11
11
 
12
- def initialize(capacity, default_value = nil, ttl:, clock: nil)
12
+ def initialize(capacity, default_value = nil, ttl:, clock: nil, &block)
13
13
  @default_value = default_value
14
14
  @ttl = ttl
15
15
  @clock = clock
16
16
 
17
+ @initialize_block = block
18
+
17
19
  @data = CappedMap.new(capacity, mode: :lru)
18
20
  end
19
21
 
@@ -38,7 +40,7 @@ module Aikido::Zen
38
40
  if key?(key)
39
41
  @data[key].value
40
42
  else
41
- @default_value
43
+ default_value
42
44
  end
43
45
  end
44
46
 
@@ -62,6 +64,10 @@ module Aikido::Zen
62
64
  def to_h
63
65
  to_a.to_h
64
66
  end
67
+
68
+ private def default_value
69
+ @default_value || @initialize_block&.call
70
+ end
65
71
  end
66
72
 
67
73
  class CacheEntry
@@ -39,7 +39,7 @@ module Aikido::Zen
39
39
  class TrackRequest < Event
40
40
  register "track_request"
41
41
 
42
- def self.from_json(data)
42
+ def self.from_json(_data)
43
43
  new
44
44
  end
45
45
 
@@ -52,6 +52,22 @@ module Aikido::Zen
52
52
  end
53
53
  end
54
54
 
55
+ class TrackRateLimitedRequest < Event
56
+ register "track_rate_limited_request"
57
+
58
+ def self.from_json(_data)
59
+ new
60
+ end
61
+
62
+ def handle(collector)
63
+ collector.handle_track_rate_limited_request
64
+ end
65
+
66
+ def inspect
67
+ "#<#{self.class.name}>"
68
+ end
69
+ end
70
+
55
71
  class TrackUserAgent < Event
56
72
  register "track_user_agent"
57
73
 
@@ -79,6 +95,33 @@ module Aikido::Zen
79
95
  end
80
96
  end
81
97
 
98
+ class TrackIPList < Event
99
+ register "track_ip_list"
100
+
101
+ def self.from_json(data)
102
+ new(data[:ip_list_keys])
103
+ end
104
+
105
+ def initialize(ip_list_keys)
106
+ super()
107
+ @ip_list_keys = ip_list_keys
108
+ end
109
+
110
+ def as_json
111
+ super.update({
112
+ ip_list_keys: @ip_list_keys
113
+ })
114
+ end
115
+
116
+ def handle(collector)
117
+ collector.handle_track_ip_list(@ip_list_keys)
118
+ end
119
+
120
+ def inspect
121
+ "#<#{self.class.name} #{@ip_list_keys.inspect}>"
122
+ end
123
+ end
124
+
82
125
  class TrackAttackWave < Event
83
126
  register "track_attack_wave"
84
127
 
@@ -8,7 +8,7 @@ module Aikido::Zen
8
8
  # Tracks information about how the Aikido Agent is used in the app.
9
9
  class Collector::Stats
10
10
  # @!visibility private
11
- attr_reader :started_at, :ended_at, :requests, :aborted_requests, :user_agents, :attack_waves, :blocked_attack_waves, :sinks
11
+ attr_reader :started_at, :ended_at, :requests, :aborted_requests, :rate_limited_requests, :user_agents, :ip_lists, :attack_waves, :blocked_attack_waves, :sinks
12
12
 
13
13
  # @!visibility private
14
14
  attr_writer :ended_at
@@ -20,7 +20,9 @@ module Aikido::Zen
20
20
  @started_at = @ended_at = nil
21
21
  @requests = 0
22
22
  @aborted_requests = 0
23
+ @rate_limited_requests = 0
23
24
  @user_agents = Hash.new { |h, k| h[k] = 0 }
25
+ @ip_lists = Hash.new { |h, k| h[k] = 0 }
24
26
  @attack_waves = 0
25
27
  @blocked_attack_waves = 0
26
28
  @sinks = Hash.new { |h, k| h[k] = Collector::SinkStats.new(k, @config) }
@@ -67,12 +69,23 @@ module Aikido::Zen
67
69
  @requests += 1
68
70
  end
69
71
 
72
+ # @return [void]
73
+ def add_rate_limited_request
74
+ @rate_limited_requests += 1
75
+ end
76
+
70
77
  # @param user_agent_keys [Array<String>] the user agent keys
71
78
  # @return [void]
72
79
  def add_user_agent(user_agent_keys)
73
80
  user_agent_keys&.each { |user_agent_key| @user_agents[user_agent_key] += 1 }
74
81
  end
75
82
 
83
+ # @param user_agent_keys [Array<String>] the user agent keys
84
+ # @return [void]
85
+ def add_ip_list(ip_list_keys)
86
+ ip_list_keys&.each { |ip_list_key| @ip_lists[ip_list_key] += 1 }
87
+ end
88
+
76
89
  # @param being_blocked [Boolean] whether the Agent blocked the request
77
90
  # @return [void]
78
91
  def add_attack_wave(being_blocked:)
@@ -109,6 +122,7 @@ module Aikido::Zen
109
122
  requests: {
110
123
  total: @requests,
111
124
  aborted: @aborted_requests,
125
+ rateLimited: @rate_limited_requests,
112
126
  attacksDetected: {
113
127
  total: total_attacks,
114
128
  blocked: total_blocked
@@ -120,6 +134,9 @@ module Aikido::Zen
120
134
  },
121
135
  userAgents: {
122
136
  breakdown: @user_agents
137
+ },
138
+ ipAddresses: {
139
+ breakdown: @ip_lists
123
140
  }
124
141
  }
125
142
  end
@@ -88,12 +88,23 @@ module Aikido::Zen
88
88
  synchronize(@stats) { |stats| stats.add_request }
89
89
  end
90
90
 
91
+ # Track stats about the rate_limited_requests
92
+ #
93
+ # @return [void]
94
+ def track_rate_limited_request
95
+ add_event(Events::TrackRateLimitedRequest.new)
96
+ end
97
+
98
+ def handle_track_rate_limited_request
99
+ synchronize(@stats) { |stats| stats.add_rate_limited_request }
100
+ end
101
+
91
102
  # Track stats about monitored and blocked user agents
92
103
  #
93
- # @param [Array<String>, nil] the user agent keys
104
+ # @param user_agent_keys [Array<String>, nil] the user agent keys
94
105
  # @return [void]
95
106
  def track_user_agent(user_agent_keys)
96
- return if user_agent_keys.nil?
107
+ return if user_agent_keys.nil? || user_agent_keys.empty?
97
108
 
98
109
  add_event(Events::TrackUserAgent.new(user_agent_keys))
99
110
  end
@@ -102,6 +113,20 @@ module Aikido::Zen
102
113
  synchronize(@stats) { |stats| stats.add_user_agent(user_agent_keys) }
103
114
  end
104
115
 
116
+ # Track stats about blocked and monitored IP lists
117
+ #
118
+ # @param ip_list_keys [Array<String>, nil]
119
+ # @return [void]
120
+ def track_ip_list(ip_list_keys)
121
+ return if ip_list_keys.nil? || ip_list_keys.empty?
122
+
123
+ add_event(Events::TrackIPList.new(ip_list_keys))
124
+ end
125
+
126
+ def handle_track_ip_list(ip_list_keys)
127
+ synchronize(@stats) { |stats| stats.add_ip_list(ip_list_keys) }
128
+ end
129
+
105
130
  # Track stats about an attack detected by our scanners.
106
131
  #
107
132
  # @param attack [Aikido::Zen::Events::AttackWave]
@@ -12,7 +12,7 @@ module Aikido::Zen
12
12
  # @return [Class, Integer, nil] The Rack middleware class or index after which
13
13
  # the Zen middleware should be inserted. When set to nil, the middleware is
14
14
  # inserted before the first middleware in the then-current middleware stack.
15
- # Defaults to ::ActionDispatch::Executor.
15
+ # Defaults to ::ActionDispatch::RemoteIp.
16
16
  attr_accessor :insert_middleware_after
17
17
 
18
18
  # @return [Boolean] whether Aikido should be turned completely off (no
@@ -95,7 +95,7 @@ module Aikido::Zen
95
95
  # the oldest seen users.
96
96
  attr_accessor :max_users_tracked
97
97
 
98
- # @return [Proc{(Aikido::Zen::Request, Symbol) => Array(Integer, Hash, #each)}]
98
+ # @return [Proc{(Aikido::Zen::Request, Symbol, reason: String=nil) => Array(Integer, Hash, #each)}]
99
99
  # Rack handler used to respond to requests from IPs, users or others blocked in the Aikido
100
100
  # dashboard.
101
101
  attr_accessor :blocked_responder
@@ -184,8 +184,12 @@ module Aikido::Zen
184
184
  # Defaults to 10,000 entries.
185
185
  attr_accessor :attack_wave_max_cache_entries
186
186
 
187
+ # @return [Integer] the maximum number of samples in the LRU cache.
188
+ # Defaults to 15 entries.
189
+ attr_accessor :attack_wave_max_cache_samples
190
+
187
191
  def initialize
188
- self.insert_middleware_after = ::ActionDispatch::Executor
192
+ self.insert_middleware_after = ::ActionDispatch::RemoteIp
189
193
  self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
190
194
  self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
191
195
  self.api_timeouts = 10
@@ -222,6 +226,7 @@ module Aikido::Zen
222
226
  self.attack_wave_min_time_between_requests = 60 * 1000 # 1 min (ms)
223
227
  self.attack_wave_min_time_between_events = 20 * 60 * 1000 # 20 min (ms)
224
228
  self.attack_wave_max_cache_entries = 10_000
229
+ self.attack_wave_max_cache_samples = 15
225
230
  end
226
231
 
227
232
  # Set the base URL for API requests.
@@ -317,10 +322,18 @@ module Aikido::Zen
317
322
  DEFAULT_DETACHED_AGENT_SOCKET_PATH = "aikido-detached-agent.%h.sock"
318
323
 
319
324
  # @!visibility private
320
- DEFAULT_BLOCKED_RESPONDER = ->(request, blocking_type) do
325
+ DEFAULT_BLOCKED_RESPONDER = ->(request, blocking_type, reason = nil) do
321
326
  message = case blocking_type
322
327
  when :ip
323
- format("Your IP address is not allowed to access this resource. (Your IP: %s)", request.ip)
328
+ "Your IP address is not allowed to access this resource. (Your IP: #{request.ip})"
329
+ when :ip_allowed_list
330
+ "Your IP address is not allowed to access this resource. (Your IP: #{request.client_ip})"
331
+ when :ip_blocked_list
332
+ if reason.nil?
333
+ "Your IP is blocked."
334
+ else
335
+ "Your IP is blocked due to #{reason}."
336
+ end
324
337
  when :user_agent
325
338
  "You are not allowed to access this resource because you have been identified as a bot."
326
339
  else
@@ -336,7 +349,7 @@ module Aikido::Zen
336
349
 
337
350
  # @!visibility private
338
351
  DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
339
- request.actor ? "actor:#{request.actor.id}" : request.ip
352
+ request.actor ? "actor:#{request.actor.id}" : request.client_ip
340
353
  }
341
354
  end
342
355
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The current context is stored in an additional Fiber instance variable and
4
+ # is though the aikido_current_context accessor methods.
5
+
6
+ class Fiber
7
+ # @api private
8
+ attr_accessor :aikido_current_context
9
+ end
10
+
11
+ # When a new Fiber is instantiated the current context of the Fiber that is
12
+ # creating the new Fiber is copied into the new Fiber.
13
+
14
+ class << Fiber
15
+ # @api private
16
+ alias_method :new__internal_for_aikido_zen, :new
17
+
18
+ def new(*args, **kwargs, &blk)
19
+ context = Fiber.current.aikido_current_context
20
+
21
+ new__internal_for_aikido_zen(*args, **kwargs) do |*args|
22
+ Fiber.current.aikido_current_context = context
23
+
24
+ blk.call(*args)
25
+ end
26
+ end
27
+ end
@@ -46,7 +46,7 @@ module Aikido::Zen::DetachedAgent
46
46
  end
47
47
 
48
48
  def calculate_rate_limits(request)
49
- @front_object.calculate_rate_limits(request.route.as_json, request.ip, request.actor.as_json)
49
+ @front_object.calculate_rate_limits(request.route.as_json, request.client_ip, request.actor.as_json)
50
50
  end
51
51
 
52
52
  # Every time a fork occurs (a new child process is created), we need to start
@@ -17,7 +17,7 @@ module Aikido::Zen::DetachedAgent
17
17
  @rate_limiter = rate_limiter
18
18
  end
19
19
 
20
- RequestKind = Struct.new(:route, :schema, :ip, :actor)
20
+ RequestKind = Struct.new(:route, :schema, :client_ip, :actor)
21
21
 
22
22
  def send_collector_events(events_data)
23
23
  events_data.each do |event_data|
@@ -114,5 +114,11 @@ module Aikido
114
114
  super
115
115
  end
116
116
  end
117
+
118
+ class OutboundConnectionBlockedError < StandardError
119
+ def initialize(connection)
120
+ super("Zen blocked an outbound connection to #{connection.host}.")
121
+ end
122
+ end
117
123
  end
118
124
  end
@@ -71,23 +71,6 @@ module Aikido::Zen
71
71
  end
72
72
 
73
73
  class AttackWave < Event
74
- # @param [Aikido::Zen::Context] a context
75
- # @return [Aikido::Zen::Events::AttackWave] an attack wave event
76
- def self.from_context(context)
77
- request = Aikido::Zen::AttackWave::Request.new(
78
- ip_address: context.request.client_ip,
79
- user_agent: context.request.user_agent,
80
- source: context.request.framework
81
- )
82
-
83
- attack = Aikido::Zen::AttackWave::Attack.new(
84
- metadata: {}, # not used yet
85
- user: context.request.actor
86
- )
87
-
88
- new(request: request, attack: attack)
89
- end
90
-
91
74
  # @return [Aikido::Zen::AttackWave::Request]
92
75
  attr_reader :request
93
76
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Aikido::Zen
4
4
  module Middleware
5
- # Middleware that rejects requests from IPs blocked in the Aikido dashboard.
5
+ # Middleware that only allows allowed IPs when allowed IPs are configured for
6
+ # any matching route in the Aikido dashboard.
6
7
  class AllowedAddressChecker
7
8
  def initialize(app, config: Aikido::Zen.config, settings: Aikido::Zen.runtime_settings)
8
9
  @app = app
@@ -13,14 +14,21 @@ module Aikido::Zen
13
14
  def call(env)
14
15
  request = Aikido::Zen::Middleware.request_from(env)
15
16
 
16
- allowed_ips = @settings.endpoints[request.route].allowed_ips
17
-
18
- if allowed_ips.empty? || allowed_ips.include?(request.ip)
17
+ if allowed?(request)
19
18
  @app.call(env)
20
19
  else
21
20
  @config.blocked_responder.call(request, :ip)
22
21
  end
23
22
  end
23
+
24
+ private def allowed?(request)
25
+ return true if @settings.bypassed_ips.include?(request.client_ip)
26
+
27
+ matches = @settings.endpoints.matched_settings(request.route)
28
+
29
+ matches.all? { |settings| settings.allowed_ips.empty? } ||
30
+ matches.any? { |settings| settings.allowed_ips.include?(request.ip) }
31
+ end
24
32
  end
25
33
  end
26
34
  end
@@ -19,10 +19,9 @@ module Aikido::Zen
19
19
  end
20
20
 
21
21
  private def protection_disabled?(request)
22
- # Bypass attack protection for allowed IPs
23
- return true if @settings.allowed_ips.include?(request.ip)
22
+ return true if @settings.bypassed_ips.include?(request.client_ip)
24
23
 
25
- !@settings.endpoints.match(request.route).all?(&:protected?)
24
+ !@settings.endpoints.matched_settings(request.route).all?(&:protected?)
26
25
  end
27
26
  end
28
27
  end
@@ -25,8 +25,7 @@ module Aikido
25
25
  request = context.request
26
26
  return false if request.nil?
27
27
 
28
- # Bypass attack wave protection for allowed IPs
29
- return false if @settings.allowed_ips.include?(request.ip)
28
+ return false if @settings.bypassed_ips.include?(request.client_ip)
30
29
 
31
30
  @zen.attack_wave_detector.attack_wave?(context)
32
31
  end
@@ -35,7 +34,26 @@ module Aikido
35
34
  # Visible for testing.
36
35
  def protect(context)
37
36
  if attack_wave?(context)
38
- attack_wave = Aikido::Zen::Events::AttackWave.from_context(context)
37
+ client_ip = context.request.client_ip
38
+
39
+ request = Aikido::Zen::AttackWave::Request.new(
40
+ ip_address: client_ip,
41
+ user_agent: context.request.user_agent,
42
+ source: context.request.framework
43
+ )
44
+
45
+ samples = @zen.attack_wave_detector.samples[client_ip].to_a
46
+
47
+ attack = Aikido::Zen::AttackWave::Attack.new(
48
+ samples: samples,
49
+ user: context.request.actor
50
+ )
51
+
52
+ attack_wave = Aikido::Zen::Events::AttackWave.new(
53
+ request: request,
54
+ attack: attack
55
+ )
56
+
39
57
  @zen.track_attack_wave(attack_wave)
40
58
  @zen.agent.report(attack_wave)
41
59
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module Middleware
5
+ class IPListChecker
6
+ def initialize(app, zen: Aikido::Zen, config: zen.config, settings: zen.runtime_settings)
7
+ @app = app
8
+ @zen = zen
9
+ @config = config
10
+ @settings = settings
11
+ end
12
+
13
+ def call(env)
14
+ request = Aikido::Zen::Middleware.request_from(env)
15
+
16
+ client_ip = request.client_ip
17
+
18
+ return @app.call(env) if bypassed_ip?(client_ip)
19
+
20
+ if !@settings.allowed_ip?(client_ip)
21
+ return @config.blocked_responder.call(request, :ip_allowed_list)
22
+ end
23
+
24
+ monitored_ip_list_keys = @settings.monitored_ip_list_keys(client_ip)
25
+ @zen.track_ip_list(monitored_ip_list_keys)
26
+
27
+ blocked_ip_lists = @settings.blocked_ip_lists.filter { |ip_list| ip_list.include?(client_ip) }
28
+
29
+ if !blocked_ip_lists.empty?
30
+ @zen.track_ip_list(blocked_ip_lists.map(&:key))
31
+
32
+ return @config.blocked_responder.call(
33
+ request,
34
+ :ip_blocked_list,
35
+ blocked_ip_lists.first.description
36
+ )
37
+ end
38
+
39
+ @app.call(env)
40
+ end
41
+
42
+ def bypassed_ip?(client_ip)
43
+ @settings.bypassed_ips.include?(client_ip)
44
+ end
45
+ end
46
+ end
47
+ end