aikido-zen 0.1.0.alpha4-x86_64-mingw-64 → 0.1.1-x86_64-mingw-64

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +19 -0
  3. data/CHANGELOG.md +16 -0
  4. data/README.md +136 -23
  5. data/Rakefile +4 -0
  6. data/benchmarks/README.md +27 -0
  7. data/benchmarks/rails7.1_sql_injection.js +74 -0
  8. data/docs/banner.svg +203 -0
  9. data/docs/config.md +123 -0
  10. data/docs/rails.md +70 -0
  11. data/lib/aikido/zen/actor.rb +1 -1
  12. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  13. data/lib/aikido/zen/agent.rb +100 -112
  14. data/lib/aikido/zen/collector/hosts.rb +15 -0
  15. data/lib/aikido/zen/collector/routes.rb +64 -0
  16. data/lib/aikido/zen/{stats → collector}/sink_stats.rb +1 -1
  17. data/lib/aikido/zen/collector/stats.rb +111 -0
  18. data/lib/aikido/zen/{stats → collector}/users.rb +6 -2
  19. data/lib/aikido/zen/collector.rb +117 -0
  20. data/lib/aikido/zen/config.rb +17 -11
  21. data/lib/aikido/zen/context.rb +8 -1
  22. data/lib/aikido/zen/errors.rb +3 -1
  23. data/lib/aikido/zen/event.rb +7 -4
  24. data/lib/aikido/zen/internals.rb +4 -0
  25. data/lib/aikido/zen/{libzen-v0.1.26.x86_64.dll → libzen-v0.1.31.x86_64.dll} +0 -0
  26. data/lib/aikido/zen/middleware/set_context.rb +4 -1
  27. data/lib/aikido/zen/rails_engine.rb +27 -18
  28. data/lib/aikido/zen/request/schema/builder.rb +0 -2
  29. data/lib/aikido/zen/request.rb +6 -0
  30. data/lib/aikido/zen/runtime_settings.rb +6 -11
  31. data/lib/aikido/zen/scanners/ssrf_scanner.rb +12 -6
  32. data/lib/aikido/zen/sinks/action_controller.rb +64 -0
  33. data/lib/aikido/zen/sinks/http.rb +1 -1
  34. data/lib/aikido/zen/sinks/pg.rb +13 -12
  35. data/lib/aikido/zen/sinks/typhoeus.rb +1 -1
  36. data/lib/aikido/zen/sinks.rb +1 -0
  37. data/lib/aikido/zen/version.rb +2 -2
  38. data/lib/aikido/zen/worker.rb +82 -0
  39. data/lib/aikido/zen.rb +55 -50
  40. data/tasklib/bench.rake +70 -0
  41. metadata +20 -9
  42. data/CODE_OF_CONDUCT.md +0 -132
  43. data/lib/aikido/zen/stats/routes.rb +0 -53
  44. data/lib/aikido/zen/stats.rb +0 -171
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Handles scheduling the heartbeats we send to the Aikido servers, managing
5
+ # runtime changes to the heartbeat interval.
6
+ class Agent::HeartbeatsManager
7
+ def initialize(worker:, settings: Aikido::Zen.runtime_settings, config: Aikido::Zen.config)
8
+ @settings = settings
9
+ @config = config
10
+ @worker = worker
11
+
12
+ @timer = nil
13
+ end
14
+
15
+ # @return [Boolean]
16
+ def running?
17
+ !!@timer&.running?
18
+ end
19
+
20
+ # @return [Boolean] whether the currently running heartbeat matches the
21
+ # expected interval in the runtime settings.
22
+ def stale_settings?
23
+ running? && @timer.execution_interval != @settings.heartbeat_interval
24
+ end
25
+
26
+ # Sets up the the timer to run the given block at the appropriate interval.
27
+ # Re-entrant, and does nothing if already running.
28
+ #
29
+ # @return [void]
30
+ def start(&task)
31
+ return if running?
32
+
33
+ if @settings.heartbeat_interval&.nonzero?
34
+ @config.logger.debug "Scheduling heartbeats every #{@settings.heartbeat_interval} seconds"
35
+ @timer = @worker.every(@settings.heartbeat_interval, run_now: false, &task)
36
+ else
37
+ @config.logger.warn(format("Heartbeat could not be set up (interval: %p)", @settings.heartbeat_interval))
38
+ end
39
+ end
40
+
41
+ # Cleans up the timer.
42
+ #
43
+ # @return [void]
44
+ def stop
45
+ return unless running?
46
+
47
+ @timer.shutdown
48
+ @timer = nil
49
+ end
50
+
51
+ # Resets the timer to start with any new settings, if needed.
52
+ #
53
+ # @return [void]
54
+ def restart(&task)
55
+ stop
56
+ start(&task)
57
+ end
58
+
59
+ # @api private
60
+ #
61
+ # @return [Integer] the current delay between events.
62
+ def interval
63
+ @settings.heartbeat_interval
64
+ end
65
+ end
66
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "concurrent"
4
4
  require_relative "event"
5
- require_relative "stats"
6
5
  require_relative "config"
7
6
  require_relative "system_info"
8
7
 
@@ -10,31 +9,89 @@ module Aikido::Zen
10
9
  # Handles the background processes that communicate with the Aikido servers,
11
10
  # including managing the runtime settings that keep the app protected.
12
11
  class Agent
13
- # @return [Aikido::Zen::Stats] the statistics collected by the agent.
14
- attr_reader :stats
12
+ # Initialize and start an agent instance.
13
+ #
14
+ # @return [Aikido::Zen::Agent]
15
+ def self.start(**opts)
16
+ new(**opts).tap(&:start!)
17
+ end
15
18
 
16
19
  def initialize(
17
- stats: Aikido::Zen::Stats.new,
18
20
  config: Aikido::Zen.config,
19
- info: Aikido::Zen.system_info,
20
- api_client: Aikido::Zen::APIClient.new
21
+ collector: Aikido::Zen.collector,
22
+ worker: Aikido::Zen::Worker.new(config: config),
23
+ api_client: Aikido::Zen::APIClient.new(config: config)
21
24
  )
22
25
  @started_at = nil
23
26
 
24
- @stats = stats
25
- @info = info
26
27
  @config = config
28
+ @worker = worker
27
29
  @api_client = api_client
28
- @timer_tasks = []
29
- @delayed_tasks = []
30
- @reporting_pool = nil
31
- @heartbeats = nil
30
+ @collector = collector
32
31
  end
33
32
 
34
33
  def started?
35
34
  !!@started_at
36
35
  end
37
36
 
37
+ def start!
38
+ @config.logger.info "Starting Aikido agent"
39
+
40
+ raise Aikido::ZenError, "Aikido Agent already started!" if started?
41
+ @started_at = Time.now.utc
42
+ @collector.start(at: @started_at)
43
+
44
+ if @config.blocking_mode?
45
+ @config.logger.info "Requests identified as attacks will be blocked"
46
+ else
47
+ @config.logger.warn "Non-blocking mode enabled! No requests will be blocked."
48
+ end
49
+
50
+ if @api_client.can_make_requests?
51
+ @config.logger.info "API Token set! Reporting has been enabled."
52
+ else
53
+ @config.logger.warn "No API Token set! Reporting has been disabled."
54
+ return
55
+ end
56
+
57
+ at_exit { stop! if started? }
58
+
59
+ report(Events::Started.new(time: @started_at)) do |response|
60
+ Aikido::Zen.runtime_settings.update_from_json(response)
61
+ @config.logger.info "Updated runtime settings."
62
+ rescue => err
63
+ @config.logger.error(err.message)
64
+ end
65
+
66
+ poll_for_setting_updates
67
+
68
+ @worker.delay(@config.initial_heartbeat_delay) do
69
+ send_heartbeat if @collector.stats.any?
70
+ end
71
+ end
72
+
73
+ # Clean up any ongoing threads, and reset the state. Called automatically
74
+ # when the process exits.
75
+ #
76
+ # @return [void]
77
+ def stop!
78
+ @config.logger.info "Stopping Aikido agent"
79
+ @started_at = nil
80
+ @worker.shutdown
81
+ end
82
+
83
+ # Respond to the runtime settings changing after being fetched from the
84
+ # Aikido servers.
85
+ #
86
+ # @return [void]
87
+ def updated_settings!
88
+ if !heartbeats.running?
89
+ heartbeats.start { send_heartbeat }
90
+ elsif heartbeats.stale_settings?
91
+ heartbeats.restart { send_heartbeat }
92
+ end
93
+ end
94
+
38
95
  # Given an Attack, report it to the Aikido server, and/or block the request
39
96
  # depending on configuration.
40
97
  #
@@ -49,8 +106,8 @@ module Aikido::Zen
49
106
  @config.logger.error("[ATTACK DETECTED] #{attack.log_message}")
50
107
  report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
51
108
 
52
- stats.add_attack(attack, being_blocked: @config.blocking_mode?)
53
- raise attack if @config.blocking_mode?
109
+ @collector.track_attack(attack)
110
+ raise attack if attack.blocked?
54
111
  end
55
112
 
56
113
  # Asynchronously reports an Event of any kind to the Aikido dashboard. If
@@ -62,7 +119,7 @@ module Aikido::Zen
62
119
  #
63
120
  # @return [void]
64
121
  def report(event)
65
- reporting_pool.post do
122
+ @worker.perform do
66
123
  response = @api_client.report(event)
67
124
  yield response if response && block_given?
68
125
  rescue Aikido::Zen::APIError, Aikido::Zen::NetworkError => err
@@ -70,53 +127,35 @@ module Aikido::Zen
70
127
  end
71
128
  end
72
129
 
73
- def start!
74
- @config.logger.info "Starting Aikido agent"
75
-
76
- raise Aikido::ZenError, "Aikido Agent already started!" if started?
77
- @started_at = Time.now.utc
78
-
79
- stats.start(@started_at)
80
-
81
- if @config.blocking_mode?
82
- @config.logger.info "Requests identified as attacks will be blocked"
83
- else
84
- @config.logger.warn "Non-blocking mode enabled! No requests will be blocked."
85
- end
86
-
87
- if @api_client.can_make_requests?
88
- @config.logger.info "API Token set! Reporting has been enabled."
89
- else
90
- @config.logger.warn "No API Token set! Reporting has been disabled."
91
- return
92
- end
93
-
94
- # Subscribe to firewall setting changes so we can correctly re-configure
95
- # the heartbeat process.
96
- Aikido::Zen.runtime_settings.add_observer(self, :setup_heartbeat)
97
-
98
- at_exit { stop! if started? }
99
-
100
- report(Events::Started.new(time: @started_at)) do |response|
101
- Aikido::Zen.runtime_settings.update_from_json(response)
102
- @config.logger.info "Updated runtime settings."
103
- end
104
-
105
- poll_for_setting_updates
106
- end
107
-
108
- def send_heartbeat
130
+ # @api private
131
+ #
132
+ # Atomically flushes all the stats stored by the agent, and sends a
133
+ # heartbeat event. Scheduled to run automatically on a recurring schedule
134
+ # when reporting is enabled.
135
+ #
136
+ # @param at [Time] the event time. Defaults to now.
137
+ # @return [void]
138
+ # @see Aikido::Zen::RuntimeSettings#heartbeat_interval
139
+ def send_heartbeat(at: Time.now.utc)
109
140
  return unless @api_client.can_make_requests?
110
141
 
111
- flushed_stats = stats.reset
112
- report(Events::Heartbeat.new(stats: flushed_stats)) do |response|
142
+ event = @collector.flush(at: at)
143
+
144
+ report(event) do |response|
113
145
  Aikido::Zen.runtime_settings.update_from_json(response)
114
146
  @config.logger.info "Updated runtime settings after heartbeat"
115
147
  end
116
148
  end
117
149
 
150
+ # @api private
151
+ #
152
+ # Sets up the timer task that polls the Aikido Runtime API for updates to
153
+ # the runtime settings every minute.
154
+ #
155
+ # @return [void]
156
+ # @see Aikido::Zen::RuntimeSettings
118
157
  def poll_for_setting_updates
119
- timer_task(every: @config.polling_interval) do
158
+ @worker.every(@config.polling_interval) do
120
159
  if @api_client.should_fetch_settings?
121
160
  Aikido::Zen.runtime_settings.update_from_json(@api_client.fetch_settings)
122
161
  @config.logger.info "Updated runtime settings after polling"
@@ -124,64 +163,13 @@ module Aikido::Zen
124
163
  end
125
164
  end
126
165
 
127
- def setup_heartbeat(settings)
128
- return unless @api_client.can_make_requests?
129
-
130
- # If the desired interval changed, then clear the current heartbeat timer
131
- # and set up a new one.
132
- if @heartbeats&.running? && @heartbeats.execution_interval != settings.heartbeat_interval
133
- @heartbeats.shutdown
134
- @heartbeats = nil
135
- setup_heartbeat(settings)
136
-
137
- # If the heartbeat timer isn't running but we know how often it should run, schedule it.
138
- elsif !@heartbeats&.running? && settings.heartbeat_interval&.nonzero?
139
- @config.logger.debug "Scheduling heartbeats every #{settings.heartbeat_interval} seconds"
140
- @heartbeats = timer_task(every: settings.heartbeat_interval, run_now: false) do
141
- send_heartbeat
142
- end
143
-
144
- elsif !@heartbeats&.running?
145
- @config.logger.debug(format("Heartbeat could not be setup (interval: %p)", settings.heartbeat_interval))
146
- end
147
-
148
- # If the server hasn't received any stats, we want to also run a one-off
149
- # heartbeat request in a minute.
150
- if !settings.received_any_stats
151
- delay(@config.initial_heartbeat_delay) { send_heartbeat if stats.any? }
152
- end
153
- end
154
-
155
- def stop!
156
- @started_at = nil
157
-
158
- @config.logger.info "Stopping Aikido agent"
159
-
160
- @timer_tasks.each { |task| task.shutdown }
161
- @delayed_tasks.each { |task| task.cancel if task.pending? }
162
-
163
- @reporting_pool&.shutdown
164
- @reporting_pool&.wait_for_termination(30)
165
- end
166
-
167
- private def reporting_pool
168
- @reporting_pool ||= Concurrent::SingleThreadExecutor.new
169
- end
170
-
171
- private def delay(delay, &task)
172
- Concurrent::ScheduledTask.execute(delay, executor: reporting_pool, &task)
173
- .tap { |task| @delayed_tasks << task }
174
- end
175
-
176
- private def timer_task(every:, **opts, &block)
177
- Concurrent::TimerTask.execute(
178
- run_now: true,
179
- interval_type: :fixed_rate,
180
- execution_interval: every,
181
- executor: reporting_pool,
182
- **opts,
183
- &block
184
- ).tap { |task| @timer_tasks << task }
166
+ private def heartbeats
167
+ @heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
168
+ config: @config,
169
+ worker: @worker
170
+ )
185
171
  end
186
172
  end
187
173
  end
174
+
175
+ require_relative "agent/heartbeats_manager"
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../capped_collections"
4
+
5
+ module Aikido::Zen
6
+ # @api private
7
+ #
8
+ # Keeps track of the hostnames to which the app has made outbound HTTP
9
+ # requests.
10
+ class Collector::Hosts < Aikido::Zen::CappedSet
11
+ def initialize(config = Aikido::Zen.config)
12
+ super(config.max_outbound_connections)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../request/schema/empty_schema"
4
+
5
+ module Aikido::Zen
6
+ # @api private
7
+ #
8
+ # Keeps track of the visited routes.
9
+ class Collector::Routes
10
+ def initialize(config = Aikido::Zen.config)
11
+ @config = config
12
+ @visits = Hash.new { |h, k| h[k] = Record.new }
13
+ end
14
+
15
+ # @param request [Aikido::Zen::Request].
16
+ # @return [self]
17
+ def add(request)
18
+ @visits[request.route].increment(request) unless request.route.nil?
19
+ self
20
+ end
21
+
22
+ def as_json
23
+ @visits.map do |route, record|
24
+ {
25
+ method: route.verb,
26
+ path: route.path,
27
+ hits: record.hits,
28
+ apispec: record.schema.as_json
29
+ }.compact
30
+ end
31
+ end
32
+
33
+ # @api private
34
+ def [](route)
35
+ @visits[route]
36
+ end
37
+
38
+ # @api private
39
+ def empty?
40
+ @visits.empty?
41
+ end
42
+
43
+ # @api private
44
+ Record = Struct.new(:hits, :schema, :samples) do
45
+ def initialize(config = Aikido::Zen.config)
46
+ super(0, Aikido::Zen::Request::Schema::EMPTY_SCHEMA, 0)
47
+ @config = config
48
+ end
49
+
50
+ def increment(request)
51
+ self.hits += 1
52
+
53
+ if sample_schema?
54
+ self.samples += 1
55
+ self.schema |= request.schema
56
+ end
57
+ end
58
+
59
+ private def sample_schema?
60
+ samples < @config.api_schema_max_samples
61
+ end
62
+ end
63
+ end
64
+ end
@@ -6,7 +6,7 @@ module Aikido::Zen
6
6
  # @api private
7
7
  #
8
8
  # Tracks data specific to a single Sink.
9
- class Stats::SinkStats
9
+ class Collector::SinkStats
10
10
  # @return [Integer] number of total calls to Sink#scan.
11
11
  attr_accessor :scans
12
12
 
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../capped_collections"
4
+
5
+ module Aikido::Zen
6
+ # @api private
7
+ #
8
+ # Tracks information about how the Aikido Agent is used in the app.
9
+ class Collector::Stats
10
+ # @!visibility private
11
+ attr_reader :started_at, :ended_at, :requests, :aborted_requests, :sinks
12
+
13
+ # @!visibility private
14
+ attr_writer :ended_at
15
+
16
+ def initialize(config = Aikido::Zen.config)
17
+ super()
18
+ @config = config
19
+ @sinks = Hash.new { |h, k| h[k] = Collector::SinkStats.new(k, @config) }
20
+ @started_at = @ended_at = nil
21
+ @requests = 0
22
+ @aborted_requests = 0
23
+ end
24
+
25
+ # @return [Boolean]
26
+ def empty?
27
+ @requests.zero? && @sinks.empty?
28
+ end
29
+
30
+ # @return [Boolean]
31
+ def any?
32
+ !empty?
33
+ end
34
+
35
+ # Track the timestamp we start tracking this series of stats.
36
+ #
37
+ # @param at [Time]
38
+ # @return [self]
39
+ def start(at = Time.now.utc)
40
+ @started_at = at
41
+ self
42
+ end
43
+
44
+ # Sets the end time for these stats block, freezes it to avoid any more
45
+ # writing to them, and compresses the timing stats in anticipation of
46
+ # sending these to the Aikido servers.
47
+ #
48
+ # @param at [Time] the time at which we're resetting, which is set as the
49
+ # ending time for the returned copy.
50
+ # @return [self]
51
+ def flush(at: Time.now.utc)
52
+ # Make sure the timing stats are compressed before copying, since we
53
+ # need these compressed when we serialize this for the API.
54
+ @sinks.each_value { |sink| sink.compress_timings(at: at) }
55
+ @ended_at = at
56
+ freeze
57
+ end
58
+
59
+ # @return [self]
60
+ def add_request
61
+ @requests += 1
62
+ self
63
+ end
64
+
65
+ # @param scan [Aikido::Zen::Scan]
66
+ # @return [self]
67
+ def add_scan(scan)
68
+ stats = @sinks[scan.sink.name]
69
+ stats.scans += 1
70
+ stats.errors += 1 if scan.errors?
71
+ stats.add_timing(scan.duration)
72
+ self
73
+ end
74
+
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]
81
+ stats.attacks += 1
82
+ stats.blocked_attacks += 1 if being_blocked
83
+ self
84
+ end
85
+
86
+ def as_json
87
+ total_attacks, total_blocked = aggregate_attacks_from_sinks
88
+ {
89
+ startedAt: @started_at.to_i * 1000,
90
+ endedAt: (@ended_at.to_i * 1000 if @ended_at),
91
+ sinks: @sinks.transform_values(&:as_json),
92
+ requests: {
93
+ total: @requests,
94
+ aborted: @aborted_requests,
95
+ attacksDetected: {
96
+ total: total_attacks,
97
+ blocked: total_blocked
98
+ }
99
+ }
100
+ }
101
+ end
102
+
103
+ private def aggregate_attacks_from_sinks
104
+ @sinks.each_value.reduce([0, 0]) { |(attacks, blocked), stats|
105
+ [attacks + stats.attacks, blocked + stats.blocked_attacks]
106
+ }
107
+ end
108
+ end
109
+ end
110
+
111
+ require_relative "sink_stats"
@@ -2,11 +2,15 @@
2
2
 
3
3
  require_relative "../capped_collections"
4
4
 
5
- class Aikido::Zen::Stats
5
+ module Aikido::Zen
6
6
  # @api private
7
7
  #
8
8
  # Keeps track of the users that were seen by the app.
9
- class Users < Aikido::Zen::CappedMap
9
+ class Collector::Users < Aikido::Zen::CappedMap
10
+ def initialize(config = Aikido::Zen.config)
11
+ super(config.max_users_tracked)
12
+ end
13
+
10
14
  def add(actor)
11
15
  if key?(actor.id)
12
16
  self[actor.id].update
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Handles collecting all the runtime statistics to report back to the Aikido
5
+ # servers.
6
+ class Collector
7
+ def initialize(config: Aikido::Zen.config)
8
+ @config = config
9
+
10
+ @stats = Concurrent::AtomicReference.new(Stats.new(@config))
11
+ @users = Concurrent::AtomicReference.new(Users.new(@config))
12
+ @hosts = Concurrent::AtomicReference.new(Hosts.new(@config))
13
+ @routes = Concurrent::AtomicReference.new(Routes.new(@config))
14
+ end
15
+
16
+ # Flush all the stats into a Heartbeat event that can be reported back to
17
+ # the Aikido servers.
18
+ #
19
+ # @param at [Time] the time at which stats collection stopped and the start
20
+ # of the new stats collection period. Defaults to now.
21
+ # @return [Aikido::Zen::Events::Heartbeat]
22
+ def flush(at: Time.now.utc)
23
+ stats = @stats.get_and_set(Stats.new(@config))
24
+ users = @users.get_and_set(Users.new(@config))
25
+ hosts = @hosts.get_and_set(Hosts.new(@config))
26
+ routes = @routes.get_and_set(Routes.new(@config))
27
+
28
+ start(at: at)
29
+ stats = stats.flush(at: at)
30
+
31
+ Events::Heartbeat.new(stats: stats, users: users, hosts: hosts, routes: routes)
32
+ end
33
+
34
+ # Sets the start time for this collection period.
35
+ #
36
+ # @param at [Time] defaults to now.
37
+ # @return [void]
38
+ def start(at: Time.now.utc)
39
+ synchronize(@stats) { |stats| stats.start(at) }
40
+ end
41
+
42
+ # Track stats about the request, record the visited endpoint, and if
43
+ # enabled, the API schema for this endpoint.
44
+ #
45
+ # @param request [Aikido::Zen::Request]
46
+ # @return [void]
47
+ def track_request(request)
48
+ synchronize(@stats) { |stats| stats.add_request }
49
+ synchronize(@routes) { |routes| routes.add(request) if request.route }
50
+ end
51
+
52
+ # Track stats about a scan performed by one of our sinks.
53
+ #
54
+ # @param scan [Aikido::Zen::Scan]
55
+ # @return [void]
56
+ def track_scan(scan)
57
+ synchronize(@stats) { |stats| stats.add_scan(scan) }
58
+ end
59
+
60
+ # Track stats about an attack detected by our scanners.
61
+ #
62
+ # @param attack [Aikido::Zen::Attack]
63
+ # @return [void]
64
+ def track_attack(attack)
65
+ synchronize(@stats) do |stats|
66
+ stats.add_attack(attack, being_blocked: attack.blocked?)
67
+ end
68
+ end
69
+
70
+ # Track an HTTP connections to an external host.
71
+ #
72
+ # @param connection [Aikido::Zen::OutboundConnection]
73
+ # @return [void]
74
+ def track_outbound(connection)
75
+ synchronize(@hosts) { |hosts| hosts.add(connection) }
76
+ end
77
+
78
+ # Track the user reported by the developer to be behind this request.
79
+ #
80
+ # @param actor [Aikido::Zen::Actor]
81
+ # @return [void]
82
+ def track_user(actor)
83
+ synchronize(@users) { |users| users.add(actor) }
84
+ end
85
+
86
+ # @api private
87
+ def routes
88
+ @routes.get
89
+ end
90
+
91
+ # @api private
92
+ def users
93
+ @users.get
94
+ end
95
+
96
+ # @api private
97
+ def hosts
98
+ @hosts.get
99
+ end
100
+
101
+ # @api private
102
+ def stats
103
+ @stats.get
104
+ end
105
+
106
+ # Atomically modify an object's state within a block, ensuring it's safe
107
+ # from other threads.
108
+ private def synchronize(object)
109
+ object.update { |obj| obj.tap { yield obj } }
110
+ end
111
+ end
112
+ end
113
+
114
+ require_relative "collector/stats"
115
+ require_relative "collector/users"
116
+ require_relative "collector/hosts"
117
+ require_relative "collector/routes"