aikido-zen 1.0.1.beta.5-arm64-darwin → 1.0.2-arm64-darwin

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +6 -0
  3. data/README.md +2 -0
  4. data/benchmarks/README.md +0 -1
  5. data/benchmarks/rails7.1_benchmark.js +1 -0
  6. data/benchmarks/rails7.1_sql_injection.js +52 -20
  7. data/docs/config.md +9 -1
  8. data/docs/proxy.md +10 -0
  9. data/docs/rails.md +55 -13
  10. data/docs/troubleshooting.md +62 -0
  11. data/lib/aikido/zen/actor.rb +34 -4
  12. data/lib/aikido/zen/agent/heartbeats_manager.rb +5 -5
  13. data/lib/aikido/zen/agent.rb +19 -17
  14. data/lib/aikido/zen/attack.rb +19 -9
  15. data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
  16. data/lib/aikido/zen/attack_wave.rb +88 -0
  17. data/lib/aikido/zen/cache.rb +91 -0
  18. data/lib/aikido/zen/capped_collections.rb +22 -4
  19. data/lib/aikido/zen/collector/event.rb +238 -0
  20. data/lib/aikido/zen/collector/hosts.rb +16 -1
  21. data/lib/aikido/zen/collector/routes.rb +13 -8
  22. data/lib/aikido/zen/collector/stats.rb +33 -22
  23. data/lib/aikido/zen/collector/users.rb +5 -3
  24. data/lib/aikido/zen/collector.rb +107 -28
  25. data/lib/aikido/zen/config.rb +54 -21
  26. data/lib/aikido/zen/context/rack_request.rb +3 -0
  27. data/lib/aikido/zen/context/rails_request.rb +3 -0
  28. data/lib/aikido/zen/context.rb +42 -9
  29. data/lib/aikido/zen/detached_agent/agent.rb +28 -27
  30. data/lib/aikido/zen/detached_agent/front_object.rb +10 -6
  31. data/lib/aikido/zen/detached_agent/server.rb +63 -26
  32. data/lib/aikido/zen/event.rb +47 -2
  33. data/lib/aikido/zen/helpers.rb +24 -0
  34. data/lib/aikido/zen/internals.rb +23 -3
  35. data/lib/aikido/zen/libzen-v0.1.48-arm64-darwin.dylib +0 -0
  36. data/lib/aikido/zen/middleware/{check_allowed_addresses.rb → allowed_address_checker.rb} +1 -1
  37. data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
  38. data/lib/aikido/zen/middleware/{set_context.rb → context_setter.rb} +1 -1
  39. data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
  40. data/lib/aikido/zen/middleware/rack_throttler.rb +3 -1
  41. data/lib/aikido/zen/middleware/request_tracker.rb +9 -4
  42. data/lib/aikido/zen/outbound_connection.rb +18 -1
  43. data/lib/aikido/zen/payload.rb +1 -1
  44. data/lib/aikido/zen/rails_engine.rb +5 -8
  45. data/lib/aikido/zen/request/rails_router.rb +17 -2
  46. data/lib/aikido/zen/request.rb +21 -36
  47. data/lib/aikido/zen/route.rb +57 -0
  48. data/lib/aikido/zen/runtime_settings/endpoints.rb +37 -8
  49. data/lib/aikido/zen/runtime_settings.rb +6 -5
  50. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +10 -7
  51. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +5 -4
  52. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +3 -2
  53. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +3 -2
  54. data/lib/aikido/zen/scanners/ssrf_scanner.rb +2 -1
  55. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +8 -2
  56. data/lib/aikido/zen/sink.rb +1 -1
  57. data/lib/aikido/zen/sinks/action_controller.rb +3 -1
  58. data/lib/aikido/zen/sinks/file.rb +45 -4
  59. data/lib/aikido/zen/sinks/kernel.rb +1 -1
  60. data/lib/aikido/zen/sinks/socket.rb +7 -0
  61. data/lib/aikido/zen/sinks_dsl.rb +12 -0
  62. data/lib/aikido/zen/system_info.rb +1 -5
  63. data/lib/aikido/zen/version.rb +2 -2
  64. data/lib/aikido/zen.rb +77 -15
  65. data/tasklib/bench.rake +1 -1
  66. data/tasklib/libzen.rake +1 -0
  67. metadata +15 -5
  68. data/lib/aikido/zen/libzen-v0.1.39-arm64-darwin.dylib +0 -0
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ class Collector
5
+ class Event
6
+ @@registry = {}
7
+
8
+ # @api protected
9
+ def self.register(type)
10
+ const_set(:TYPE, type)
11
+ @@registry[type] = self
12
+ end
13
+
14
+ def self.from_json(data)
15
+ type = data[:type]
16
+ subclass = @@registry[type]
17
+ subclass.from_json(data)
18
+ end
19
+
20
+ attr_reader :type
21
+
22
+ def initialize
23
+ @type = self.class::TYPE
24
+ end
25
+
26
+ def as_json
27
+ {
28
+ type: @type
29
+ }
30
+ end
31
+
32
+ def handle(collector)
33
+ raise NotImplementedError, "implement in subclasses"
34
+ end
35
+ end
36
+
37
+ # @api private
38
+ module Events
39
+ class TrackRequest < Event
40
+ register "track_request"
41
+
42
+ def self.from_json(data)
43
+ new
44
+ end
45
+
46
+ def handle(collector)
47
+ collector.handle_track_request
48
+ end
49
+
50
+ def inspect
51
+ "#<#{self.class.name}>"
52
+ end
53
+ end
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
+
84
+ class TrackScan < Event
85
+ register "track_scan"
86
+
87
+ def self.from_json(data)
88
+ new(
89
+ data[:sink_name],
90
+ data[:duration],
91
+ has_errors: data[:has_errors]
92
+ )
93
+ end
94
+
95
+ def initialize(sink_name, duration, has_errors:)
96
+ super()
97
+ @sink_name = sink_name
98
+ @duration = duration
99
+ @has_errors = has_errors
100
+ end
101
+
102
+ def as_json
103
+ super.update({
104
+ sink_name: @sink_name,
105
+ duration: @duration,
106
+ has_errors: @has_errors
107
+ })
108
+ end
109
+
110
+ def handle(collector)
111
+ collector.handle_track_scan(@sink_name, @duration, has_errors: @has_errors)
112
+ end
113
+
114
+ def inspect
115
+ "#<#{self.class.name} #{@sink_name} #{format "%0.6f", @duration} #{@has_errors}>"
116
+ end
117
+ end
118
+
119
+ class TrackAttack < Event
120
+ register "track_attack"
121
+
122
+ def self.from_json(data)
123
+ new(
124
+ data[:sink_name],
125
+ being_blocked: data[:being_blocked]
126
+ )
127
+ end
128
+
129
+ def initialize(sink_name, being_blocked:)
130
+ super()
131
+ @sink_name = sink_name
132
+ @being_blocked = being_blocked
133
+ end
134
+
135
+ def as_json
136
+ super.update({
137
+ sink_name: @sink_name,
138
+ being_blocked: @being_blocked
139
+ })
140
+ end
141
+
142
+ def handle(collector)
143
+ collector.handle_track_attack(@sink_name, being_blocked: @being_blocked)
144
+ end
145
+
146
+ def inspect
147
+ "#<#{self.class.name} #{@sink_name} #{@being_blocked}>"
148
+ end
149
+ end
150
+
151
+ class TrackUser < Event
152
+ register "track_user"
153
+
154
+ def self.from_json(data)
155
+ new(Aikido::Zen::Actor.from_json(data[:actor]))
156
+ end
157
+
158
+ def initialize(actor)
159
+ super()
160
+ @actor = actor
161
+ end
162
+
163
+ def as_json
164
+ super.update({
165
+ actor: @actor.as_json
166
+ })
167
+ end
168
+
169
+ def handle(collector)
170
+ collector.handle_track_user(@actor)
171
+ end
172
+
173
+ def inspect
174
+ "#<#{self.class.name} #{@actor.id} #{@actor.name}>"
175
+ end
176
+ end
177
+
178
+ class TrackOutbound < Event
179
+ register "track_outbound"
180
+
181
+ def self.from_json(data)
182
+ new(OutboundConnection.from_json(data[:connection]))
183
+ end
184
+
185
+ def initialize(connection)
186
+ super()
187
+ @connection = connection
188
+ end
189
+
190
+ def as_json
191
+ super.update({
192
+ connection: @connection.as_json
193
+ })
194
+ end
195
+
196
+ def handle(collector)
197
+ collector.handle_track_outbound(@connection)
198
+ end
199
+
200
+ def inspect
201
+ "#<#{self.class.name} #{@connection.host}:#{@connection.port}>"
202
+ end
203
+ end
204
+
205
+ class TrackRoute < Event
206
+ register "track_route"
207
+
208
+ def self.from_json(data)
209
+ new(
210
+ Route.from_json(data[:route]),
211
+ Request::Schema.from_json(data[:schema])
212
+ )
213
+ end
214
+
215
+ def initialize(route, schema)
216
+ super()
217
+ @route = route
218
+ @schema = schema
219
+ end
220
+
221
+ def as_json
222
+ super.update({
223
+ route: @route.as_json,
224
+ schema: @schema.as_json
225
+ })
226
+ end
227
+
228
+ def handle(collector)
229
+ collector.handle_track_route(@route, @schema)
230
+ end
231
+
232
+ def inspect
233
+ "#<#{self.class.name} #{@route.verb} #{@route.path.inspect}>"
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -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
@@ -7,6 +7,8 @@ module Aikido::Zen
7
7
  #
8
8
  # Keeps track of the visited routes.
9
9
  class Collector::Routes
10
+ # @api private
11
+ # Visible for testing.
10
12
  attr_reader :visits
11
13
 
12
14
  def initialize(config = Aikido::Zen.config)
@@ -14,11 +16,11 @@ module Aikido::Zen
14
16
  @visits = Hash.new { |h, k| h[k] = Record.new }
15
17
  end
16
18
 
17
- # @param request [Aikido::Zen::Request].
18
- # @return [self]
19
- def add(request)
20
- @visits[request.route].increment(request) unless request.route.nil?
21
- self
19
+ # @param route [Aikido::Zen::Route] the route of the request
20
+ # @param schema [Aikido::Zen::Request::Schema] the schema for the request
21
+ # @return [void]
22
+ def add(route, schema)
23
+ @visits[route].increment(schema) unless route.nil?
22
24
  end
23
25
 
24
26
  def as_json
@@ -33,6 +35,7 @@ module Aikido::Zen
33
35
  end
34
36
 
35
37
  # @api private
38
+ # Visible for testing.
36
39
  def [](route)
37
40
  @visits[route]
38
41
  end
@@ -49,16 +52,18 @@ module Aikido::Zen
49
52
  @config = config
50
53
  end
51
54
 
52
- def increment(request)
55
+ def increment(schema)
53
56
  self.hits += 1
54
57
 
55
58
  if sample_schema?
56
59
  self.samples += 1
57
- self.schema |= request.schema
60
+ self.schema |= schema
58
61
  end
59
62
  end
60
63
 
61
- private def sample_schema?
64
+ private
65
+
66
+ def sample_schema?
62
67
  samples < @config.api_schema_max_samples
63
68
  end
64
69
  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]
@@ -35,10 +38,9 @@ module Aikido::Zen
35
38
  # Track the timestamp we start tracking this series of stats.
36
39
  #
37
40
  # @param at [Time]
38
- # @return [self]
41
+ # @return [void]
39
42
  def start(at = Time.now.utc)
40
43
  @started_at = at
41
- self
42
44
  end
43
45
 
44
46
  # Sets the end time for these stats block, freezes it to avoid any more
@@ -47,7 +49,7 @@ module Aikido::Zen
47
49
  #
48
50
  # @param at [Time] the time at which we're resetting, which is set as the
49
51
  # ending time for the returned copy.
50
- # @return [self]
52
+ # @return [void]
51
53
  def flush(at: Time.now.utc)
52
54
  # Make sure the timing stats are compressed before copying, since we
53
55
  # need these compressed when we serialize this for the API.
@@ -56,31 +58,36 @@ module Aikido::Zen
56
58
  freeze
57
59
  end
58
60
 
59
- # @return [self]
61
+ # @return [void]
60
62
  def add_request
61
63
  @requests += 1
62
- self
63
64
  end
64
65
 
65
- # @param scan [Aikido::Zen::Scan]
66
- # @return [self]
67
- def add_scan(scan)
68
- stats = @sinks[scan.sink.name]
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
+
73
+ # @param sink_name [String] the name of the sink
74
+ # @param duration [Float] the length the scan in seconds
75
+ # @param has_errors [Boolean] whether errors occurred during the scan
76
+ # @return [void]
77
+ def add_scan(sink_name, duration, has_errors:)
78
+ stats = @sinks[sink_name]
69
79
  stats.scans += 1
70
- stats.errors += 1 if scan.errors?
71
- stats.add_timing(scan.duration)
72
- self
80
+ stats.errors += 1 if has_errors
81
+ stats.add_timing(duration)
73
82
  end
74
83
 
75
- # @param attack [Aikido::Zen::Attack]
76
- # @param being_blocked [Boolean] whether the Agent blocked the
77
- # request where this Attack happened or not.
78
- # @return [self]
79
- def add_attack(attack, being_blocked:)
80
- stats = @sinks[attack.sink.name]
84
+ # @param sink_name [String] the name of the sink
85
+ # @param being_blocked [Boolean] whether the Agent blocked the request
86
+ # @return [void]
87
+ def add_attack(sink_name, being_blocked:)
88
+ stats = @sinks[sink_name]
81
89
  stats.attacks += 1
82
90
  stats.blocked_attacks += 1 if being_blocked
83
- self
84
91
  end
85
92
 
86
93
  def as_json
@@ -88,13 +95,17 @@ module Aikido::Zen
88
95
  {
89
96
  startedAt: @started_at.to_i * 1000,
90
97
  endedAt: (@ended_at.to_i * 1000 if @ended_at),
91
- sinks: @sinks.transform_values(&:as_json),
98
+ operations: @sinks.transform_values(&:as_json),
92
99
  requests: {
93
100
  total: @requests,
94
101
  aborted: @aborted_requests,
95
102
  attacksDetected: {
96
103
  total: total_attacks,
97
104
  blocked: total_blocked
105
+ },
106
+ attackWaves: {
107
+ total: @attack_waves,
108
+ blocked: @blocked_attack_waves
98
109
  }
99
110
  }
100
111
  }
@@ -11,16 +11,18 @@ module Aikido::Zen
11
11
  super(config.max_users_tracked)
12
12
  end
13
13
 
14
+ # @param actor [Aikido::Zen::Actor]
15
+ # @return [void]
14
16
  def add(actor)
15
17
  if key?(actor.id)
16
- self[actor.id].update
18
+ self[actor.id] |= actor
17
19
  else
18
20
  self[actor.id] = actor
19
21
  end
20
22
  end
21
23
 
22
- def each(&b)
23
- each_value(&b)
24
+ def each(&blk)
25
+ each_value(&blk)
24
26
  end
25
27
 
26
28
  def as_json
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "collector/event"
4
+
3
5
  module Aikido::Zen
4
6
  # Handles collecting all the runtime statistics to report back to the Aikido
5
7
  # servers.
@@ -7,14 +9,44 @@ module Aikido::Zen
7
9
  def initialize(config: Aikido::Zen.config)
8
10
  @config = config
9
11
 
12
+ @events = Queue.new
13
+
10
14
  @stats = Concurrent::AtomicReference.new(Stats.new(@config))
11
15
  @users = Concurrent::AtomicReference.new(Users.new(@config))
12
16
  @hosts = Concurrent::AtomicReference.new(Hosts.new(@config))
13
17
  @routes = Concurrent::AtomicReference.new(Routes.new(@config))
14
- @heartbeats = Queue.new
18
+
15
19
  @middleware_installed = Concurrent::AtomicBoolean.new
16
20
  end
17
21
 
22
+ # Add an event, to be handled in the collector in current process or sent
23
+ # to a collector in another process.
24
+ #
25
+ # @param event [Aikido::Zen::Collector::Event] the event to add
26
+ # @return [void]
27
+ def add_event(event)
28
+ @events << event
29
+ end
30
+
31
+ # @return [Boolean] whether the collector has any events
32
+ def has_events?
33
+ !@events.empty?
34
+ end
35
+
36
+ # Flush all events.
37
+ #
38
+ # @return [Array<Aikido::Zen::Collector::Event>]
39
+ def flush_events
40
+ Array.new(@events.size) { @events.pop }
41
+ end
42
+
43
+ # Handle the events in the queue.
44
+ #
45
+ # @return [void]
46
+ def handle
47
+ flush_events.each { |event| event.handle(self) }
48
+ end
49
+
18
50
  # Flush all the stats into a Heartbeat event that can be reported back to
19
51
  # the Aikido servers.
20
52
  #
@@ -22,6 +54,8 @@ module Aikido::Zen
22
54
  # of the new stats collection period. Defaults to now.
23
55
  # @return [Aikido::Zen::Events::Heartbeat]
24
56
  def flush(at: Time.now.utc)
57
+ handle
58
+
25
59
  stats = @stats.get_and_set(Stats.new(@config))
26
60
  users = @users.get_and_set(Users.new(@config))
27
61
  hosts = @hosts.get_and_set(Hosts.new(@config))
@@ -30,21 +64,11 @@ module Aikido::Zen
30
64
  start(at: at)
31
65
  stats = stats.flush(at: at)
32
66
 
33
- Events::Heartbeat.new(
67
+ Aikido::Zen::Events::Heartbeat.new(
34
68
  stats: stats, users: users, hosts: hosts, routes: routes, middleware_installed: middleware_installed?
35
69
  )
36
70
  end
37
71
 
38
- # Put heartbeats coming from child processes into the internal queue.
39
- def push_heartbeat(heartbeat)
40
- @heartbeats << heartbeat
41
- end
42
-
43
- # Drains into an array all the queued heartbeats
44
- def flush_heartbeats
45
- Array.new(@heartbeats.size) { @heartbeats.pop }
46
- end
47
-
48
72
  # Sets the start time for this collection period.
49
73
  #
50
74
  # @param at [Time] defaults to now.
@@ -55,16 +79,27 @@ module Aikido::Zen
55
79
 
56
80
  # Track stats about the requests
57
81
  #
58
- # @param request [Aikido::Zen::Request]
59
82
  # @return [void]
60
- def track_request(*)
83
+ def track_request
84
+ add_event(Events::TrackRequest.new)
85
+ end
86
+
87
+ def handle_track_request
61
88
  synchronize(@stats) { |stats| stats.add_request }
62
89
  end
63
90
 
64
- # Record the visited endpoint, and if enabled, the API schema for this endpoint.
65
- # @param request [Aikido::Zen::Request]
66
- def track_route(request)
67
- synchronize(@routes) { |routes| routes.add(request) if request.route }
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
68
103
  end
69
104
 
70
105
  # Track stats about a scan performed by one of our sinks.
@@ -72,7 +107,13 @@ module Aikido::Zen
72
107
  # @param scan [Aikido::Zen::Scan]
73
108
  # @return [void]
74
109
  def track_scan(scan)
75
- synchronize(@stats) { |stats| stats.add_scan(scan) }
110
+ add_event(Events::TrackScan.new(scan.sink.name, scan.duration, has_errors: scan.errors?))
111
+ end
112
+
113
+ def handle_track_scan(sink_name, duration, has_errors:)
114
+ synchronize(@stats) do |stats|
115
+ stats.add_scan(sink_name, duration, has_errors: has_errors)
116
+ end
76
117
  end
77
118
 
78
119
  # Track stats about an attack detected by our scanners.
@@ -80,25 +121,49 @@ module Aikido::Zen
80
121
  # @param attack [Aikido::Zen::Attack]
81
122
  # @return [void]
82
123
  def track_attack(attack)
124
+ add_event(Events::TrackAttack.new(attack.sink.name, being_blocked: attack.blocked?))
125
+ end
126
+
127
+ def handle_track_attack(sink_name, being_blocked:)
83
128
  synchronize(@stats) do |stats|
84
- stats.add_attack(attack, being_blocked: attack.blocked?)
129
+ stats.add_attack(sink_name, being_blocked: being_blocked)
85
130
  end
86
131
  end
87
132
 
133
+ # Track the user reported by the developer to be behind this request.
134
+ #
135
+ # @param actor [Aikido::Zen::Actor]
136
+ # @return [void]
137
+ def track_user(actor)
138
+ add_event(Events::TrackUser.new(actor))
139
+ end
140
+
141
+ def handle_track_user(actor)
142
+ synchronize(@users) { |users| users.add(actor) }
143
+ end
144
+
88
145
  # Track an HTTP connections to an external host.
89
146
  #
90
147
  # @param connection [Aikido::Zen::OutboundConnection]
91
148
  # @return [void]
92
149
  def track_outbound(connection)
150
+ add_event(Events::TrackOutbound.new(connection))
151
+ end
152
+
153
+ def handle_track_outbound(connection)
93
154
  synchronize(@hosts) { |hosts| hosts.add(connection) }
94
155
  end
95
156
 
96
- # Track the user reported by the developer to be behind this request.
157
+ # Record the visited endpoint, and if enabled, the API schema for this endpoint.
97
158
  #
98
- # @param actor [Aikido::Zen::Actor]
159
+ # @param request [Aikido::Zen::Request]
99
160
  # @return [void]
100
- def track_user(actor)
101
- synchronize(@users) { |users| users.add(actor) }
161
+ def track_route(request)
162
+ add_event(Events::TrackRoute.new(request.route, request.schema))
163
+ end
164
+
165
+ def handle_track_route(route, schema)
166
+ synchronize(@routes) { |routes| routes.add(route, schema) }
102
167
  end
103
168
 
104
169
  def middleware_installed!
@@ -106,26 +171,40 @@ module Aikido::Zen
106
171
  end
107
172
 
108
173
  # @api private
109
- def routes
110
- @routes.get
174
+ #
175
+ # @note Visible for testing.
176
+ def stats
177
+ handle
178
+ @stats.get
111
179
  end
112
180
 
113
181
  # @api private
182
+ #
183
+ # @note Visible for testing.
114
184
  def users
185
+ handle
115
186
  @users.get
116
187
  end
117
188
 
118
189
  # @api private
190
+ #
191
+ # @note Visible for testing.
119
192
  def hosts
193
+ handle
120
194
  @hosts.get
121
195
  end
122
196
 
123
197
  # @api private
124
- def stats
125
- @stats.get
198
+ #
199
+ # @note Visible for testing.
200
+ def routes
201
+ handle
202
+ @routes.get
126
203
  end
127
204
 
128
205
  # @api private
206
+ #
207
+ # @note Visible for testing.
129
208
  def middleware_installed?
130
209
  @middleware_installed.true?
131
210
  end