aikido-zen 0.1.0.alpha4-arm64-linux → 0.1.1-arm64-linux

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/.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.31.aarch64.so +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/libzen-v0.1.26.aarch64.so +0 -0
  44. data/lib/aikido/zen/stats/routes.rb +0 -53
  45. 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"