aikido-zen 1.0.1.beta.2-arm64-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 (115) hide show
  1. checksums.yaml +7 -0
  2. data/.aikido +6 -0
  3. data/.ruby-version +1 -0
  4. data/.simplecov +26 -0
  5. data/.standard.yml +3 -0
  6. data/LICENSE +674 -0
  7. data/README.md +146 -0
  8. data/Rakefile +67 -0
  9. data/benchmarks/README.md +23 -0
  10. data/benchmarks/rails7.1_sql_injection.js +70 -0
  11. data/docs/banner.svg +202 -0
  12. data/docs/config.md +125 -0
  13. data/docs/rails.md +70 -0
  14. data/lib/aikido/zen/actor.rb +116 -0
  15. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  16. data/lib/aikido/zen/agent.rb +179 -0
  17. data/lib/aikido/zen/api_client.rb +142 -0
  18. data/lib/aikido/zen/attack.rb +207 -0
  19. data/lib/aikido/zen/background_worker.rb +52 -0
  20. data/lib/aikido/zen/capped_collections.rb +68 -0
  21. data/lib/aikido/zen/collector/hosts.rb +15 -0
  22. data/lib/aikido/zen/collector/routes.rb +66 -0
  23. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  24. data/lib/aikido/zen/collector/stats.rb +111 -0
  25. data/lib/aikido/zen/collector/users.rb +30 -0
  26. data/lib/aikido/zen/collector.rb +144 -0
  27. data/lib/aikido/zen/config.rb +279 -0
  28. data/lib/aikido/zen/context/rack_request.rb +24 -0
  29. data/lib/aikido/zen/context/rails_request.rb +42 -0
  30. data/lib/aikido/zen/context.rb +112 -0
  31. data/lib/aikido/zen/detached_agent/agent.rb +78 -0
  32. data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
  33. data/lib/aikido/zen/detached_agent/server.rb +41 -0
  34. data/lib/aikido/zen/detached_agent.rb +2 -0
  35. data/lib/aikido/zen/errors.rb +107 -0
  36. data/lib/aikido/zen/event.rb +71 -0
  37. data/lib/aikido/zen/internals.rb +102 -0
  38. data/lib/aikido/zen/libzen-v0.1.39-arm64-linux-musl.so +0 -0
  39. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
  40. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  41. data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
  42. data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
  43. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  44. data/lib/aikido/zen/outbound_connection.rb +45 -0
  45. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  46. data/lib/aikido/zen/package.rb +22 -0
  47. data/lib/aikido/zen/payload.rb +50 -0
  48. data/lib/aikido/zen/rails_engine.rb +70 -0
  49. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  50. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  51. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  52. data/lib/aikido/zen/rate_limiter.rb +50 -0
  53. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  54. data/lib/aikido/zen/request/rails_router.rb +72 -0
  55. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  56. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  57. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  58. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  59. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  60. data/lib/aikido/zen/request/schema.rb +87 -0
  61. data/lib/aikido/zen/request.rb +103 -0
  62. data/lib/aikido/zen/route.rb +39 -0
  63. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  64. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  65. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  66. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  67. data/lib/aikido/zen/runtime_settings.rb +65 -0
  68. data/lib/aikido/zen/scan.rb +75 -0
  69. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  70. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
  71. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  72. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
  73. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -0
  74. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  75. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  76. data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
  77. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
  78. data/lib/aikido/zen/scanners.rb +7 -0
  79. data/lib/aikido/zen/sink.rb +118 -0
  80. data/lib/aikido/zen/sinks/action_controller.rb +83 -0
  81. data/lib/aikido/zen/sinks/async_http.rb +82 -0
  82. data/lib/aikido/zen/sinks/curb.rb +115 -0
  83. data/lib/aikido/zen/sinks/em_http.rb +85 -0
  84. data/lib/aikido/zen/sinks/excon.rb +121 -0
  85. data/lib/aikido/zen/sinks/file.rb +116 -0
  86. data/lib/aikido/zen/sinks/http.rb +95 -0
  87. data/lib/aikido/zen/sinks/httpclient.rb +97 -0
  88. data/lib/aikido/zen/sinks/httpx.rb +80 -0
  89. data/lib/aikido/zen/sinks/kernel.rb +34 -0
  90. data/lib/aikido/zen/sinks/mysql2.rb +33 -0
  91. data/lib/aikido/zen/sinks/net_http.rb +103 -0
  92. data/lib/aikido/zen/sinks/patron.rb +105 -0
  93. data/lib/aikido/zen/sinks/pg.rb +74 -0
  94. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  95. data/lib/aikido/zen/sinks/socket.rb +80 -0
  96. data/lib/aikido/zen/sinks/sqlite3.rb +49 -0
  97. data/lib/aikido/zen/sinks/trilogy.rb +33 -0
  98. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  99. data/lib/aikido/zen/sinks.rb +39 -0
  100. data/lib/aikido/zen/sinks_dsl.rb +226 -0
  101. data/lib/aikido/zen/synchronizable.rb +24 -0
  102. data/lib/aikido/zen/system_info.rb +84 -0
  103. data/lib/aikido/zen/version.rb +10 -0
  104. data/lib/aikido/zen/worker.rb +87 -0
  105. data/lib/aikido/zen.rb +206 -0
  106. data/lib/aikido-zen.rb +3 -0
  107. data/placeholder/.gitignore +4 -0
  108. data/placeholder/README.md +11 -0
  109. data/placeholder/Rakefile +75 -0
  110. data/placeholder/lib/placeholder.rb.template +3 -0
  111. data/placeholder/placeholder.gemspec.template +20 -0
  112. data/tasklib/bench.rake +94 -0
  113. data/tasklib/libzen.rake +132 -0
  114. data/tasklib/wrk.rb +88 -0
  115. metadata +204 -0
@@ -0,0 +1,52 @@
1
+ module Aikido::Zen
2
+ # Generic background worker class backed by queue. Meant to be used by any
3
+ # background process that needs to do heavy tasks.
4
+ class BackgroundWorker
5
+ # @param block [block] A block that receives 1 message directly from the queue
6
+ def initialize(&block)
7
+ @queue = Queue.new
8
+ @block = block
9
+ end
10
+
11
+ # starts the background thread, blocking the thread until a new messages arrives
12
+ # or the queue is stopped.
13
+ def start
14
+ @thread = Thread.new do
15
+ while running? || actions?
16
+ action = wait_for_action
17
+ @block.call(action) unless action.nil?
18
+ end
19
+ end
20
+ end
21
+
22
+ def restart
23
+ stop
24
+ @queue = Queue.new # re-open the queue
25
+ start
26
+ end
27
+
28
+ # Drain the queue to do not lose any messages
29
+ def stop
30
+ @queue.close # stop accepting messages
31
+ @thread.join # wait for the queue to be drained
32
+ end
33
+
34
+ def enqueue(scan)
35
+ @queue.push(scan)
36
+ end
37
+
38
+ private
39
+
40
+ def actions?
41
+ !@queue.empty?
42
+ end
43
+
44
+ def running?
45
+ !@queue.closed?
46
+ end
47
+
48
+ def wait_for_action
49
+ @queue.pop(false)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Aikido::Zen
6
+ # @api private
7
+ #
8
+ # Provides a FIFO set with a maximum size. Adding an element after the
9
+ # capacity has been reached kicks the oldest element in the set out,
10
+ # while maintaining the uniqueness property of a set (relying on #eql?
11
+ # and #hash).
12
+ class CappedSet
13
+ include Enumerable
14
+ extend Forwardable
15
+
16
+ def_delegators :@data, :size, :empty?
17
+
18
+ # @return [Integer]
19
+ attr_reader :capacity
20
+
21
+ def initialize(capacity)
22
+ @data = CappedMap.new(capacity)
23
+ end
24
+
25
+ def <<(element)
26
+ @data[element] = nil
27
+ self
28
+ end
29
+ alias_method :add, :<<
30
+ alias_method :push, :<<
31
+
32
+ def each(&b)
33
+ @data.each_key(&b)
34
+ end
35
+
36
+ def as_json
37
+ map(&:as_json)
38
+ end
39
+ end
40
+
41
+ # @api private
42
+ #
43
+ # Provides a FIFO hash-like structure with a maximum size. Adding a new key
44
+ # after the capacity has been reached kicks the first element pair added out.
45
+ class CappedMap
46
+ include Enumerable
47
+ extend Forwardable
48
+
49
+ def_delegators :@data,
50
+ :[], :fetch, :delete, :key?,
51
+ :each, :each_key, :each_value,
52
+ :size, :empty?, :to_hash
53
+
54
+ # @return [Integer]
55
+ attr_reader :capacity
56
+
57
+ def initialize(capacity)
58
+ raise ArgumentError, "cannot set capacity lower than 1: #{capacity}" if capacity < 1
59
+ @capacity = capacity
60
+ @data = {}
61
+ end
62
+
63
+ def []=(key, value)
64
+ @data[key] = value
65
+ @data.delete(@data.each_key.first) if @data.size > @capacity
66
+ end
67
+ end
68
+ end
@@ -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,66 @@
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
+ attr_reader :visits
11
+
12
+ def initialize(config = Aikido::Zen.config)
13
+ @config = config
14
+ @visits = Hash.new { |h, k| h[k] = Record.new }
15
+ end
16
+
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
22
+ end
23
+
24
+ def as_json
25
+ @visits.map do |route, record|
26
+ {
27
+ method: route.verb,
28
+ path: route.path,
29
+ hits: record.hits,
30
+ apispec: record.schema.as_json
31
+ }.compact
32
+ end
33
+ end
34
+
35
+ # @api private
36
+ def [](route)
37
+ @visits[route]
38
+ end
39
+
40
+ # @api private
41
+ def empty?
42
+ @visits.empty?
43
+ end
44
+
45
+ # @api private
46
+ Record = Struct.new(:hits, :schema, :samples) do
47
+ def initialize(config = Aikido::Zen.config)
48
+ super(0, Aikido::Zen::Request::Schema::EMPTY_SCHEMA, 0)
49
+ @config = config
50
+ end
51
+
52
+ def increment(request)
53
+ self.hits += 1
54
+
55
+ if sample_schema?
56
+ self.samples += 1
57
+ self.schema |= request.schema
58
+ end
59
+ end
60
+
61
+ private def sample_schema?
62
+ samples < @config.api_schema_max_samples
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../capped_collections"
4
+
5
+ module Aikido::Zen
6
+ # @api private
7
+ #
8
+ # Tracks data specific to a single Sink.
9
+ class Collector::SinkStats
10
+ # @return [Integer] number of total calls to Sink#scan.
11
+ attr_accessor :scans
12
+
13
+ # @return [Integer] number of scans where our scanners raised an
14
+ # error that was handled.
15
+ attr_accessor :errors
16
+
17
+ # @return [Integer] number of scans where an attack was detected.
18
+ attr_accessor :attacks
19
+
20
+ # @return [Integer] number of scans where an attack was detected
21
+ # _and_ blocked by the Zen.
22
+ attr_accessor :blocked_attacks
23
+
24
+ # @return [Set<Float>] keeps the duration of individual scans. If
25
+ # this grows to match Config#max_performance_samples, the set is
26
+ # cleared and the data is aggregated into #compressed_timings.
27
+ attr_accessor :timings
28
+
29
+ # @return [Array<CompressedTiming>] list of aggregated stats.
30
+ attr_accessor :compressed_timings
31
+
32
+ def initialize(name, config)
33
+ @name = name
34
+ @config = config
35
+
36
+ @scans = 0
37
+ @errors = 0
38
+
39
+ @attacks = 0
40
+ @blocked_attacks = 0
41
+
42
+ @timings = Set.new
43
+ @compressed_timings = CappedSet.new(@config.max_compressed_stats)
44
+ end
45
+
46
+ def add_timing(duration)
47
+ compress_timings if @timings.size >= @config.max_performance_samples
48
+ @timings << duration
49
+ end
50
+
51
+ def compress_timings(at: Time.now.utc)
52
+ return if @timings.empty?
53
+
54
+ list = @timings.sort
55
+ @timings.clear
56
+
57
+ mean = list.sum / list.size
58
+ percentiles = percentiles(list, 50, 75, 90, 95, 99)
59
+
60
+ @compressed_timings << CompressedTiming.new(mean, percentiles, at)
61
+ end
62
+
63
+ def as_json
64
+ {
65
+ total: @scans,
66
+ interceptorThrewError: @errors,
67
+ withoutContext: 0,
68
+ attacksDetected: {
69
+ total: @attacks,
70
+ blocked: @blocked_attacks
71
+ },
72
+ compressedTimings: @compressed_timings.as_json
73
+ }
74
+ end
75
+
76
+ private def percentiles(sorted, *scores)
77
+ return {} if sorted.empty? || scores.empty?
78
+
79
+ scores.map { |p|
80
+ index = (sorted.size * (p / 100.0)).floor
81
+ [p, sorted.at(index)]
82
+ }.to_h
83
+ end
84
+
85
+ CompressedTiming = Struct.new(:mean, :percentiles, :compressed_at) do
86
+ def as_json
87
+ {
88
+ averageInMs: mean * 1000,
89
+ percentiles: percentiles.transform_values { |t| t * 1000 },
90
+ compressedAt: compressed_at.to_i * 1000
91
+ }
92
+ end
93
+ end
94
+ end
95
+ end
@@ -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"
@@ -0,0 +1,30 @@
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 users that were seen by the app.
9
+ class Collector::Users < Aikido::Zen::CappedMap
10
+ def initialize(config = Aikido::Zen.config)
11
+ super(config.max_users_tracked)
12
+ end
13
+
14
+ def add(actor)
15
+ if key?(actor.id)
16
+ self[actor.id].update
17
+ else
18
+ self[actor.id] = actor
19
+ end
20
+ end
21
+
22
+ def each(&b)
23
+ each_value(&b)
24
+ end
25
+
26
+ def as_json
27
+ map(&:as_json)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,144 @@
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
+ @heartbeats = Queue.new
15
+ @middleware_installed = Concurrent::AtomicBoolean.new
16
+ end
17
+
18
+ # Flush all the stats into a Heartbeat event that can be reported back to
19
+ # the Aikido servers.
20
+ #
21
+ # @param at [Time] the time at which stats collection stopped and the start
22
+ # of the new stats collection period. Defaults to now.
23
+ # @return [Aikido::Zen::Events::Heartbeat]
24
+ def flush(at: Time.now.utc)
25
+ stats = @stats.get_and_set(Stats.new(@config))
26
+ users = @users.get_and_set(Users.new(@config))
27
+ hosts = @hosts.get_and_set(Hosts.new(@config))
28
+ routes = @routes.get_and_set(Routes.new(@config))
29
+
30
+ start(at: at)
31
+ stats = stats.flush(at: at)
32
+
33
+ Events::Heartbeat.new(
34
+ stats: stats, users: users, hosts: hosts, routes: routes, middleware_installed: middleware_installed?
35
+ )
36
+ end
37
+
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
+ # Sets the start time for this collection period.
49
+ #
50
+ # @param at [Time] defaults to now.
51
+ # @return [void]
52
+ def start(at: Time.now.utc)
53
+ synchronize(@stats) { |stats| stats.start(at) }
54
+ end
55
+
56
+ # Track stats about the requests
57
+ #
58
+ # @param request [Aikido::Zen::Request]
59
+ # @return [void]
60
+ def track_request(*)
61
+ synchronize(@stats) { |stats| stats.add_request }
62
+ end
63
+
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 }
68
+ end
69
+
70
+ # Track stats about a scan performed by one of our sinks.
71
+ #
72
+ # @param scan [Aikido::Zen::Scan]
73
+ # @return [void]
74
+ def track_scan(scan)
75
+ synchronize(@stats) { |stats| stats.add_scan(scan) }
76
+ end
77
+
78
+ # Track stats about an attack detected by our scanners.
79
+ #
80
+ # @param attack [Aikido::Zen::Attack]
81
+ # @return [void]
82
+ def track_attack(attack)
83
+ synchronize(@stats) do |stats|
84
+ stats.add_attack(attack, being_blocked: attack.blocked?)
85
+ end
86
+ end
87
+
88
+ # Track an HTTP connections to an external host.
89
+ #
90
+ # @param connection [Aikido::Zen::OutboundConnection]
91
+ # @return [void]
92
+ def track_outbound(connection)
93
+ synchronize(@hosts) { |hosts| hosts.add(connection) }
94
+ end
95
+
96
+ # Track the user reported by the developer to be behind this request.
97
+ #
98
+ # @param actor [Aikido::Zen::Actor]
99
+ # @return [void]
100
+ def track_user(actor)
101
+ synchronize(@users) { |users| users.add(actor) }
102
+ end
103
+
104
+ def middleware_installed!
105
+ @middleware_installed.make_true
106
+ end
107
+
108
+ # @api private
109
+ def routes
110
+ @routes.get
111
+ end
112
+
113
+ # @api private
114
+ def users
115
+ @users.get
116
+ end
117
+
118
+ # @api private
119
+ def hosts
120
+ @hosts.get
121
+ end
122
+
123
+ # @api private
124
+ def stats
125
+ @stats.get
126
+ end
127
+
128
+ # @api private
129
+ def middleware_installed?
130
+ @middleware_installed.true?
131
+ end
132
+
133
+ # Atomically modify an object's state within a block, ensuring it's safe
134
+ # from other threads.
135
+ private def synchronize(object)
136
+ object.update { |obj| obj.tap { yield obj } }
137
+ end
138
+ end
139
+ end
140
+
141
+ require_relative "collector/stats"
142
+ require_relative "collector/users"
143
+ require_relative "collector/hosts"
144
+ require_relative "collector/routes"