aikido-zen 1.0.2.beta.10-x86_64-linux-musl → 1.0.2-x86_64-linux-musl

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/docs/config.md +1 -1
  4. data/docs/troubleshooting.md +62 -0
  5. data/lib/aikido/zen/agent.rb +2 -2
  6. data/lib/aikido/zen/attack.rb +8 -6
  7. data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
  8. data/lib/aikido/zen/attack_wave.rb +88 -0
  9. data/lib/aikido/zen/cache.rb +91 -0
  10. data/lib/aikido/zen/capped_collections.rb +22 -4
  11. data/lib/aikido/zen/collector/event.rb +29 -0
  12. data/lib/aikido/zen/collector/hosts.rb +16 -1
  13. data/lib/aikido/zen/collector/stats.rb +17 -3
  14. data/lib/aikido/zen/collector/users.rb +2 -2
  15. data/lib/aikido/zen/collector.rb +14 -0
  16. data/lib/aikido/zen/config.rb +29 -6
  17. data/lib/aikido/zen/context/rack_request.rb +3 -0
  18. data/lib/aikido/zen/context/rails_request.rb +3 -0
  19. data/lib/aikido/zen/context.rb +2 -2
  20. data/lib/aikido/zen/event.rb +47 -2
  21. data/lib/aikido/zen/helpers.rb +24 -0
  22. data/lib/aikido/zen/middleware/{check_allowed_addresses.rb → allowed_address_checker.rb} +1 -1
  23. data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
  24. data/lib/aikido/zen/middleware/{set_context.rb → context_setter.rb} +1 -1
  25. data/lib/aikido/zen/middleware/rack_throttler.rb +3 -1
  26. data/lib/aikido/zen/middleware/request_tracker.rb +8 -3
  27. data/lib/aikido/zen/outbound_connection.rb +11 -1
  28. data/lib/aikido/zen/rails_engine.rb +3 -2
  29. data/lib/aikido/zen/request/rails_router.rb +17 -2
  30. data/lib/aikido/zen/request.rb +2 -36
  31. data/lib/aikido/zen/route.rb +50 -0
  32. data/lib/aikido/zen/runtime_settings/endpoints.rb +37 -8
  33. data/lib/aikido/zen/runtime_settings.rb +5 -4
  34. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +3 -2
  35. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +3 -2
  36. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +3 -2
  37. data/lib/aikido/zen/scanners/ssrf_scanner.rb +2 -1
  38. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +5 -1
  39. data/lib/aikido/zen/sinks/action_controller.rb +3 -1
  40. data/lib/aikido/zen/sinks/socket.rb +7 -0
  41. data/lib/aikido/zen/system_info.rb +1 -5
  42. data/lib/aikido/zen/version.rb +1 -1
  43. data/lib/aikido/zen.rb +55 -6
  44. data/tasklib/bench.rake +1 -1
  45. metadata +10 -4
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ class Cache
5
+ extend Forwardable
6
+
7
+ # @api private
8
+ # Visible for testing.
9
+ def_delegators :@data,
10
+ :size, :empty?
11
+
12
+ def initialize(capacity, default_value = nil, ttl:, clock: nil)
13
+ @default_value = default_value
14
+ @ttl = ttl
15
+ @clock = clock
16
+
17
+ @data = CappedMap.new(capacity, mode: :lru)
18
+ end
19
+
20
+ def key?(key)
21
+ @data.key?(key) && !@data[key].expired?
22
+ end
23
+
24
+ # @param key [Object] the key
25
+ # @param value [Object] the value
26
+ # @return [Object] the value that the key was set to
27
+ def []=(key, value)
28
+ if key?(key)
29
+ entry = @data[key]
30
+ entry.refresh
31
+ entry.value = value
32
+ else
33
+ @data[key] = CacheEntry.new(value, ttl: @ttl, clock: @clock)
34
+ end
35
+ end
36
+
37
+ def [](key)
38
+ if key?(key)
39
+ @data[key].value
40
+ else
41
+ @default_value
42
+ end
43
+ end
44
+
45
+ def delete(key)
46
+ if key?(key)
47
+ @data.delete(key).value
48
+ else
49
+ @data.delete(key)
50
+ nil
51
+ end
52
+ end
53
+
54
+ # @api private
55
+ # Visible for testing.
56
+ def to_a
57
+ @data.map { |key, entry| [key, entry.value] }
58
+ end
59
+
60
+ # @api private
61
+ # Visible for testing.
62
+ def to_h
63
+ to_a.to_h
64
+ end
65
+ end
66
+
67
+ class CacheEntry
68
+ attr_accessor :value
69
+
70
+ DEFAULT_CLOCK = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) }
71
+
72
+ # @param value [Object] the value
73
+ # @param ttl [Integer] the time-to-live in milliseconds
74
+ # @return [Aikido::Zen::CacheEntry]
75
+ def initialize(value, ttl:, clock: nil)
76
+ @value = value
77
+ @ttl = ttl
78
+ @clock = clock || DEFAULT_CLOCK
79
+
80
+ refresh
81
+ end
82
+
83
+ def refresh
84
+ @expires = @clock.call + @ttl
85
+ end
86
+
87
+ def expired?
88
+ @clock.call >= @expires
89
+ end
90
+ end
91
+ end
@@ -18,8 +18,8 @@ module Aikido::Zen
18
18
  # @return [Integer]
19
19
  attr_reader :capacity
20
20
 
21
- def initialize(capacity)
22
- @data = CappedMap.new(capacity)
21
+ def initialize(capacity, mode: :fifo)
22
+ @data = CappedMap.new(capacity, mode: mode)
23
23
  end
24
24
 
25
25
  def <<(element)
@@ -47,16 +47,23 @@ module Aikido::Zen
47
47
  extend Forwardable
48
48
 
49
49
  def_delegators :@data,
50
- :[], :fetch, :delete, :key?,
50
+ :delete, :key?,
51
51
  :each, :each_key, :each_value,
52
52
  :size, :empty?, :to_hash
53
53
 
54
54
  # @return [Integer]
55
55
  attr_reader :capacity
56
56
 
57
- def initialize(capacity)
57
+ def initialize(capacity, mode: :fifo)
58
58
  raise ArgumentError, "cannot set capacity lower than 1: #{capacity}" if capacity < 1
59
+
60
+ unless [:fifo, :lru].include?(mode)
61
+ raise ArgumentError, "unsupported mode: #{mode}"
62
+ end
63
+
59
64
  @capacity = capacity
65
+ @mode = mode
66
+
60
67
  @data = {}
61
68
  end
62
69
 
@@ -64,5 +71,16 @@ module Aikido::Zen
64
71
  @data[key] = value
65
72
  @data.delete(@data.each_key.first) if @data.size > @capacity
66
73
  end
74
+
75
+ def [](key)
76
+ @data[key] = @data.delete(key) if @mode == :lru && key?(key)
77
+ @data[key]
78
+ end
79
+
80
+ def fetch(key, ...)
81
+ return self[key] if key?(key)
82
+
83
+ @data.fetch(key, ...)
84
+ end
67
85
  end
68
86
  end
@@ -52,6 +52,35 @@ module Aikido::Zen
52
52
  end
53
53
  end
54
54
 
55
+ class TrackAttackWave < Event
56
+ register "track_attack_wave"
57
+
58
+ def self.from_json(data)
59
+ new(
60
+ being_blocked: data[:being_blocked]
61
+ )
62
+ end
63
+
64
+ def initialize(being_blocked:)
65
+ super()
66
+ @being_blocked = being_blocked
67
+ end
68
+
69
+ def as_json
70
+ super.update({
71
+ being_blocked: @being_blocked
72
+ })
73
+ end
74
+
75
+ def handle(collector)
76
+ collector.handle_track_attack_wave(being_blocked: @being_blocked)
77
+ end
78
+
79
+ def inspect
80
+ "#<#{self.class.name} #{@being_blocked}>"
81
+ end
82
+ end
83
+
55
84
  class TrackScan < Event
56
85
  register "track_scan"
57
86
 
@@ -7,9 +7,24 @@ module Aikido::Zen
7
7
  #
8
8
  # Keeps track of the hostnames to which the app has made outbound HTTP
9
9
  # requests.
10
- class Collector::Hosts < Aikido::Zen::CappedSet
10
+ class Collector::Hosts < Aikido::Zen::CappedMap
11
11
  def initialize(config = Aikido::Zen.config)
12
12
  super(config.max_outbound_connections)
13
13
  end
14
+
15
+ # @param host [Aikido::Zen::OutboundConnection]
16
+ # @return [void]
17
+ def add(host)
18
+ self[host] ||= host
19
+ self[host].hit
20
+ end
21
+
22
+ def each(&blk)
23
+ each_value(&blk)
24
+ end
25
+
26
+ def as_json
27
+ map(&:as_json)
28
+ end
14
29
  end
15
30
  end
@@ -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, :sinks
11
+ attr_reader :started_at, :ended_at, :requests, :aborted_requests, :attack_waves, :blocked_attack_waves, :sinks
12
12
 
13
13
  # @!visibility private
14
14
  attr_writer :ended_at
@@ -16,10 +16,13 @@ module Aikido::Zen
16
16
  def initialize(config = Aikido::Zen.config)
17
17
  super()
18
18
  @config = config
19
- @sinks = Hash.new { |h, k| h[k] = Collector::SinkStats.new(k, @config) }
19
+
20
20
  @started_at = @ended_at = nil
21
21
  @requests = 0
22
22
  @aborted_requests = 0
23
+ @attack_waves = 0
24
+ @blocked_attack_waves = 0
25
+ @sinks = Hash.new { |h, k| h[k] = Collector::SinkStats.new(k, @config) }
23
26
  end
24
27
 
25
28
  # @return [Boolean]
@@ -60,6 +63,13 @@ module Aikido::Zen
60
63
  @requests += 1
61
64
  end
62
65
 
66
+ # @param being_blocked [Boolean] whether the Agent blocked the request
67
+ # @return [void]
68
+ def add_attack_wave(being_blocked:)
69
+ @attack_waves += 1
70
+ @blocked_attack_waves += 1 if being_blocked
71
+ end
72
+
63
73
  # @param sink_name [String] the name of the sink
64
74
  # @param duration [Float] the length the scan in seconds
65
75
  # @param has_errors [Boolean] whether errors occurred during the scan
@@ -85,13 +95,17 @@ module Aikido::Zen
85
95
  {
86
96
  startedAt: @started_at.to_i * 1000,
87
97
  endedAt: (@ended_at.to_i * 1000 if @ended_at),
88
- sinks: @sinks.transform_values(&:as_json),
98
+ operations: @sinks.transform_values(&:as_json),
89
99
  requests: {
90
100
  total: @requests,
91
101
  aborted: @aborted_requests,
92
102
  attacksDetected: {
93
103
  total: total_attacks,
94
104
  blocked: total_blocked
105
+ },
106
+ attackWaves: {
107
+ total: @attack_waves,
108
+ blocked: @blocked_attack_waves
95
109
  }
96
110
  }
97
111
  }
@@ -21,8 +21,8 @@ module Aikido::Zen
21
21
  end
22
22
  end
23
23
 
24
- def each(&b)
25
- each_value(&b)
24
+ def each(&blk)
25
+ each_value(&blk)
26
26
  end
27
27
 
28
28
  def as_json
@@ -88,6 +88,20 @@ module Aikido::Zen
88
88
  synchronize(@stats) { |stats| stats.add_request }
89
89
  end
90
90
 
91
+ # Track stats about an attack detected by our scanners.
92
+ #
93
+ # @param attack [Aikido::Zen::Events::AttackWave]
94
+ # @return [void]
95
+ def track_attack_wave(being_blocked:)
96
+ add_event(Events::TrackAttackWave.new(being_blocked: being_blocked))
97
+ end
98
+
99
+ def handle_track_attack_wave(being_blocked:)
100
+ synchronize(@stats) do |stats|
101
+ stats.add_attack_wave(being_blocked: being_blocked)
102
+ end
103
+ end
104
+
91
105
  # Track stats about a scan performed by one of our sinks.
92
106
  #
93
107
  # @param scan [Aikido::Zen::Scan]
@@ -11,7 +11,7 @@ module Aikido::Zen
11
11
  # @return [Boolean] whether Aikido should be turned completely off (no
12
12
  # intercepting calls to protect the app, no agent process running, no
13
13
  # middleware installed). Defaults to false (so, enabled). Can be set
14
- # via the AIKIDO_DISABLED environment variable.
14
+ # via the AIKIDO_DISABLE environment variable.
15
15
  attr_accessor :disabled
16
16
  alias_method :disabled?, :disabled
17
17
 
@@ -158,15 +158,34 @@ module Aikido::Zen
158
158
  attr_accessor :harden
159
159
  alias_method :harden?, :harden
160
160
 
161
+ # @return [Integer] how many suspicious requests are allowed before an
162
+ # attack wave detected event is reported.
163
+ # Defaults to 15 requests.
164
+ attr_accessor :attack_wave_threshold
165
+
166
+ # @return [Integer] the minimum time in milliseconds between requests for
167
+ # requests to be part of an attack wave.
168
+ # Defaults to 1 minute in milliseconds.
169
+ attr_accessor :attack_wave_min_time_between_requests
170
+
171
+ # @return [Integer] the minimum time in milliseconds between reporting
172
+ # attack wave events.
173
+ # Defaults to 20 minutes in milliseconds.
174
+ attr_accessor :attack_wave_min_time_between_events
175
+
176
+ # @return [Integer] the maximum number of entries in the LRU cache.
177
+ # Defaults to 10,000 entries.
178
+ attr_accessor :attack_wave_max_cache_entries
179
+
161
180
  def initialize
162
- self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
181
+ self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
163
182
  self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
164
183
  self.api_timeouts = 10
165
184
  self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT", DEFAULT_AIKIDO_ENDPOINT)
166
185
  self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
167
186
  self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
168
- self.polling_interval = 60
169
- self.initial_heartbeat_delays = [30, 60 * 2]
187
+ self.polling_interval = 60 # 1 min
188
+ self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
170
189
  self.json_encoder = DEFAULT_JSON_ENCODER
171
190
  self.json_decoder = DEFAULT_JSON_DECODER
172
191
  self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
@@ -181,8 +200,8 @@ module Aikido::Zen
181
200
  self.blocked_responder = DEFAULT_BLOCKED_RESPONDER
182
201
  self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
183
202
  self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
184
- self.server_rate_limit_deadline = 1800 # 30 min
185
- self.client_rate_limit_period = 3600 # 1 hour
203
+ self.server_rate_limit_deadline = 30 * 60 # 30 min
204
+ self.client_rate_limit_period = 60 * 60 # 1 hour
186
205
  self.client_rate_limit_max_events = 100
187
206
  self.collect_api_schema = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", true))
188
207
  self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
@@ -191,6 +210,10 @@ module Aikido::Zen
191
210
  self.stored_ssrf = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_STORED_SSRF", true))
192
211
  self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
193
212
  self.harden = read_boolean_from_env(ENV.fetch("AIKIDO_HARDEN", true))
213
+ self.attack_wave_threshold = 15
214
+ self.attack_wave_min_time_between_requests = 60 * 1000 # 1 min (ms)
215
+ self.attack_wave_min_time_between_events = 20 * 60 * 1000 # 20 min (ms)
216
+ self.attack_wave_max_cache_entries = 10_000
194
217
  end
195
218
 
196
219
  # Set the base URL for API requests.
@@ -6,6 +6,9 @@ require_relative "../request/heuristic_router"
6
6
  module Aikido::Zen
7
7
  # @!visibility private
8
8
  Context::RACK_REQUEST_BUILDER = ->(env) do
9
+ # Normalize PATH_INFO so routes are correctly recognized in middleware.
10
+ env["PATH_INFO"] = Helpers.normalize_path(env["PATH_INFO"])
11
+
9
12
  delegate = Rack::Request.new(env)
10
13
  router = Aikido::Zen::Request::HeuristicRouter.new
11
14
  request = Aikido::Zen::Request.new(delegate, framework: "rack", router: router)
@@ -12,6 +12,9 @@ module Aikido::Zen
12
12
 
13
13
  # @!visibility private
14
14
  Context::RAILS_REQUEST_BUILDER = ->(env) do
15
+ # Normalize PATH_INFO so routes are correctly recognized in middleware.
16
+ env["PATH_INFO"] = Helpers.normalize_path(env["PATH_INFO"])
17
+
15
18
  # Duplicate the Rack environment to prevent unexpected modifications from
16
19
  # breaking Rails routing.
17
20
  delegate = ActionDispatch::Request.new(env.dup)
@@ -81,8 +81,8 @@ module Aikido::Zen
81
81
  def protection_disabled?
82
82
  return false if request.nil?
83
83
 
84
- !@settings.endpoints[request.route].protected? ||
85
- @settings.skip_protection_for_ips.include?(request.ip)
84
+ !@settings.endpoints.match(request.route).all?(&:protected?) ||
85
+ @settings.allowed_ips.include?(request.ip)
86
86
  end
87
87
 
88
88
  # @!visibility private
@@ -41,8 +41,10 @@ module Aikido::Zen
41
41
 
42
42
  def as_json
43
43
  super.update(
44
- attack: @attack.as_json,
45
- request: @attack.context.request.as_json
44
+ {
45
+ attack: @attack.as_json,
46
+ request: @attack.context&.request&.as_json
47
+ }.compact
46
48
  )
47
49
  end
48
50
  end
@@ -67,5 +69,48 @@ module Aikido::Zen
67
69
  )
68
70
  end
69
71
  end
72
+
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
+ # @return [Aikido::Zen::AttackWave::Request]
92
+ attr_reader :request
93
+
94
+ # @return [Aikido::Zen::AttackWave::Attack]
95
+ attr_reader :attack
96
+
97
+ # @param [Aikido::Zen::AttackWave::Request] the attack wave request
98
+ # @param [Aikido::Zen::AttackWave::Attack] the attack wave attack
99
+ # @param opts [Hash<Symbol, Object>] any other options to pass to
100
+ # the superclass initializer.
101
+ # @return [Aikido::Zen::Events::AttackWave] an attack wave event
102
+ def initialize(request:, attack:, **opts)
103
+ super(type: "detected_attack_wave", **opts)
104
+ @request = request
105
+ @attack = attack
106
+ end
107
+
108
+ def as_json
109
+ super.update(
110
+ request: @request.as_json,
111
+ attack: @attack.as_json
112
+ )
113
+ end
114
+ end
70
115
  end
71
116
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido
4
+ module Zen
5
+ # @api private
6
+ module Helpers
7
+ # Normalizes a path by:
8
+ #
9
+ # 1. Collapsing consecutive forward slashes into a single forward slash.
10
+ # 2. Removing forward trailing slash, unless the normalized path is "/".
11
+ #
12
+ # @param path [String, nil] the path to normalize.
13
+ # @return [String, nil] the normalized path.
14
+ def self.normalize_path(path)
15
+ return path unless path
16
+
17
+ normalized_path = path.dup
18
+ normalized_path.squeeze!("/")
19
+ normalized_path.chomp!("/") unless normalized_path == "/"
20
+ normalized_path
21
+ end
22
+ end
23
+ end
24
+ end
@@ -3,7 +3,7 @@
3
3
  module Aikido::Zen
4
4
  module Middleware
5
5
  # Middleware that rejects requests from IPs blocked in the Aikido dashboard.
6
- class CheckAllowedAddresses
6
+ class AllowedAddressChecker
7
7
  def initialize(app, config: Aikido::Zen.config, settings: Aikido::Zen.runtime_settings)
8
8
  @app = app
9
9
  @config = config
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido
4
+ module Zen
5
+ module Middleware
6
+ class AttackWaveProtector
7
+ def initialize(app, zen: Aikido::Zen, settings: Aikido::Zen.runtime_settings)
8
+ @app = app
9
+ @zen = zen
10
+ @settings = settings
11
+ end
12
+
13
+ def call(env)
14
+ response = @app.call(env)
15
+
16
+ context = @zen.current_context
17
+ protect(context)
18
+
19
+ response
20
+ end
21
+
22
+ # @api private
23
+ # Visible for testing.
24
+ def attack_wave?(context)
25
+ request = context.request
26
+ return false if request.nil?
27
+
28
+ # Bypass attack wave protection for allowed IPs
29
+ return false if @settings.allowed_ips.include?(request.ip)
30
+
31
+ @zen.attack_wave_detector.attack_wave?(context)
32
+ end
33
+
34
+ # @api private
35
+ # Visible for testing.
36
+ def protect(context)
37
+ if attack_wave?(context)
38
+ attack_wave = Aikido::Zen::Events::AttackWave.from_context(context)
39
+ @zen.track_attack_wave(attack_wave)
40
+ @zen.agent.report(attack_wave)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -9,7 +9,7 @@ module Aikido::Zen
9
9
  module Middleware
10
10
  # Rack middleware that keeps the current context in a Thread/Fiber-local
11
11
  # variable so that other parts of the agent/firewall can access it.
12
- class SetContext
12
+ class ContextSetter
13
13
  def initialize(app)
14
14
  @app = app
15
15
  end
@@ -33,8 +33,10 @@ module Aikido::Zen
33
33
  private
34
34
 
35
35
  def should_throttle?(request)
36
+ # Bypass rate limiting for allowed IPs
37
+ return false if @settings.allowed_ips.include?(request.ip)
38
+
36
39
  return false unless @settings.endpoints[request.route].rate_limiting.enabled?
37
- return false if @settings.skip_protection_for_ips.include?(request.ip)
38
40
 
39
41
  result = @detached_agent.calculate_rate_limits(request)
40
42
 
@@ -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)
8
+ def initialize(app, settings: Aikido::Zen.runtime_settings)
9
9
  @app = app
10
+ @settings = settings
10
11
  end
11
12
 
12
13
  def call(env)
@@ -16,7 +17,8 @@ module Aikido::Zen
16
17
  if request.route && track?(
17
18
  status_code: response[0],
18
19
  route: request.route.path,
19
- http_method: request.request_method
20
+ http_method: request.request_method,
21
+ ip: request.ip
20
22
  )
21
23
  Aikido::Zen.track_request(request)
22
24
 
@@ -126,7 +128,10 @@ module Aikido::Zen
126
128
  # @param status_code [Integer]
127
129
  # @param route [String]
128
130
  # @param http_method [String]
129
- def track?(status_code:, route:, http_method:)
131
+ def track?(status_code:, route:, http_method:, ip: nil)
132
+ # Bypass request and route tracking for allowed IPs
133
+ return false if @settings.allowed_ips.include?(ip)
134
+
130
135
  # In the UI we want to show only successful (2xx) or redirect (3xx) responses
131
136
  # anything else is discarded.
132
137
  return false unless status_code >= 200 && status_code <= 399
@@ -25,13 +25,23 @@ module Aikido::Zen
25
25
  # @return [Integer] the port number to which the connection was attempted.
26
26
  attr_reader :port
27
27
 
28
+ # @return [Integer] the number of times that this connection was seen by
29
+ # the hosts collector.
30
+ attr_reader :hits
31
+
28
32
  def initialize(host:, port:)
29
33
  @host = host
30
34
  @port = port
31
35
  end
32
36
 
37
+ def hit
38
+ # Lazy initialize @hits, so it stays nil until the connection is tracked.
39
+ @hits ||= 0
40
+ @hits += 1
41
+ end
42
+
33
43
  def as_json
34
- {hostname: host, port: port}
44
+ {hostname: host, port: port, hits: hits}.compact
35
45
  end
36
46
 
37
47
  def ==(other)
@@ -12,8 +12,9 @@ module Aikido::Zen
12
12
  initializer "aikido.add_middleware" do |app|
13
13
  app.middleware.insert_before 0, Aikido::Zen::Middleware::ForkDetector
14
14
 
15
- app.middleware.use Aikido::Zen::Middleware::SetContext
16
- app.middleware.use Aikido::Zen::Middleware::CheckAllowedAddresses
15
+ app.middleware.use Aikido::Zen::Middleware::ContextSetter
16
+ app.middleware.use Aikido::Zen::Middleware::AllowedAddressChecker
17
+ app.middleware.use Aikido::Zen::Middleware::AttackWaveProtector
17
18
  # Request Tracker stats do not consider failed request or 40x, so the middleware
18
19
  # must be the last one wrapping the request.
19
20
  app.middleware.use Aikido::Zen::Middleware::RequestTracker
@@ -62,16 +62,31 @@ module Aikido::Zen
62
62
  nil
63
63
  end
64
64
 
65
- private def build_route(route, request, prefix: request.script_name)
65
+ private
66
+
67
+ def build_route(route, request, prefix: request.script_name)
66
68
  route_wrapper = ActionDispatch::Routing::RouteWrapper.new(route)
67
69
 
68
70
  path = if prefix.present?
69
- File.join(prefix.to_s, route_wrapper.path).chomp("/")
71
+ prefix_route_path(prefix.to_s, route_wrapper.path)
70
72
  else
71
73
  route_wrapper.path
72
74
  end
73
75
 
74
76
  Aikido::Zen::Route.new(verb: request.request_method, path: path)
75
77
  end
78
+
79
+ def prefix_route_path(string1, string2)
80
+ # The strings appear to start with "/", allowing them to be concatenated
81
+ # directly after removing trailing "/". However, as it is not currently
82
+ # known whether this is guaranteed, we insert a separator when necessary.
83
+
84
+ separator = string2.start_with?("/") ? "" : "/"
85
+
86
+ string1 = string1.chomp("/")
87
+ string2 = string2.chomp("/")
88
+
89
+ "#{string1}#{separator}#{string2}"
90
+ end
76
91
  end
77
92
  end