aikido-zen 1.0.2.beta.2-aarch64-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 (116) 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/proxy.md +10 -0
  14. data/docs/rails.md +114 -0
  15. data/lib/aikido/zen/actor.rb +116 -0
  16. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  17. data/lib/aikido/zen/agent.rb +179 -0
  18. data/lib/aikido/zen/api_client.rb +145 -0
  19. data/lib/aikido/zen/attack.rb +207 -0
  20. data/lib/aikido/zen/background_worker.rb +52 -0
  21. data/lib/aikido/zen/capped_collections.rb +68 -0
  22. data/lib/aikido/zen/collector/hosts.rb +15 -0
  23. data/lib/aikido/zen/collector/routes.rb +66 -0
  24. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  25. data/lib/aikido/zen/collector/stats.rb +111 -0
  26. data/lib/aikido/zen/collector/users.rb +30 -0
  27. data/lib/aikido/zen/collector.rb +144 -0
  28. data/lib/aikido/zen/config.rb +282 -0
  29. data/lib/aikido/zen/context/rack_request.rb +24 -0
  30. data/lib/aikido/zen/context/rails_request.rb +44 -0
  31. data/lib/aikido/zen/context.rb +112 -0
  32. data/lib/aikido/zen/detached_agent/agent.rb +78 -0
  33. data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
  34. data/lib/aikido/zen/detached_agent/server.rb +78 -0
  35. data/lib/aikido/zen/detached_agent.rb +2 -0
  36. data/lib/aikido/zen/errors.rb +107 -0
  37. data/lib/aikido/zen/event.rb +71 -0
  38. data/lib/aikido/zen/internals.rb +103 -0
  39. data/lib/aikido/zen/libzen-v0.1.39-aarch64-linux.so +0 -0
  40. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
  41. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  42. data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
  43. data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
  44. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  45. data/lib/aikido/zen/outbound_connection.rb +45 -0
  46. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  47. data/lib/aikido/zen/package.rb +22 -0
  48. data/lib/aikido/zen/payload.rb +50 -0
  49. data/lib/aikido/zen/rails_engine.rb +56 -0
  50. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  51. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  52. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  53. data/lib/aikido/zen/rate_limiter.rb +50 -0
  54. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  55. data/lib/aikido/zen/request/rails_router.rb +77 -0
  56. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  57. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  58. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  59. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  60. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  61. data/lib/aikido/zen/request/schema.rb +87 -0
  62. data/lib/aikido/zen/request.rb +122 -0
  63. data/lib/aikido/zen/route.rb +39 -0
  64. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  65. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  66. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  67. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  68. data/lib/aikido/zen/runtime_settings.rb +65 -0
  69. data/lib/aikido/zen/scan.rb +75 -0
  70. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  71. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
  72. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  73. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
  74. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -0
  75. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  76. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  77. data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
  78. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
  79. data/lib/aikido/zen/scanners.rb +7 -0
  80. data/lib/aikido/zen/sink.rb +118 -0
  81. data/lib/aikido/zen/sinks/action_controller.rb +83 -0
  82. data/lib/aikido/zen/sinks/async_http.rb +80 -0
  83. data/lib/aikido/zen/sinks/curb.rb +113 -0
  84. data/lib/aikido/zen/sinks/em_http.rb +83 -0
  85. data/lib/aikido/zen/sinks/excon.rb +118 -0
  86. data/lib/aikido/zen/sinks/file.rb +112 -0
  87. data/lib/aikido/zen/sinks/http.rb +93 -0
  88. data/lib/aikido/zen/sinks/httpclient.rb +95 -0
  89. data/lib/aikido/zen/sinks/httpx.rb +78 -0
  90. data/lib/aikido/zen/sinks/kernel.rb +33 -0
  91. data/lib/aikido/zen/sinks/mysql2.rb +31 -0
  92. data/lib/aikido/zen/sinks/net_http.rb +101 -0
  93. data/lib/aikido/zen/sinks/patron.rb +103 -0
  94. data/lib/aikido/zen/sinks/pg.rb +72 -0
  95. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  96. data/lib/aikido/zen/sinks/socket.rb +78 -0
  97. data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
  98. data/lib/aikido/zen/sinks/trilogy.rb +31 -0
  99. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  100. data/lib/aikido/zen/sinks.rb +36 -0
  101. data/lib/aikido/zen/sinks_dsl.rb +250 -0
  102. data/lib/aikido/zen/synchronizable.rb +24 -0
  103. data/lib/aikido/zen/system_info.rb +84 -0
  104. data/lib/aikido/zen/version.rb +10 -0
  105. data/lib/aikido/zen/worker.rb +87 -0
  106. data/lib/aikido/zen.rb +246 -0
  107. data/lib/aikido-zen.rb +3 -0
  108. data/placeholder/.gitignore +4 -0
  109. data/placeholder/README.md +11 -0
  110. data/placeholder/Rakefile +75 -0
  111. data/placeholder/lib/placeholder.rb.template +3 -0
  112. data/placeholder/placeholder.gemspec.template +20 -0
  113. data/tasklib/bench.rake +94 -0
  114. data/tasklib/libzen.rake +133 -0
  115. data/tasklib/wrk.rb +88 -0
  116. metadata +205 -0
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Attack objects gather information about a type of detected attack.
5
+ # They can be used in a few ways, like for reporting an attack event
6
+ # to the Aikido server, or can be raised as errors to block requests
7
+ # if blocking_mode is on.
8
+ class Attack
9
+ attr_reader :context
10
+ attr_reader :operation
11
+ attr_accessor :sink
12
+
13
+ def initialize(context:, sink:, operation:)
14
+ @context = context
15
+ @operation = operation
16
+ @sink = sink
17
+ @blocked = false
18
+ end
19
+
20
+ def will_be_blocked!
21
+ @blocked = true
22
+ end
23
+
24
+ def blocked?
25
+ @blocked
26
+ end
27
+
28
+ def humanized_name
29
+ raise NotImplementedError, "implement in subclasses"
30
+ end
31
+
32
+ def kind
33
+ raise NotImplementedError, "implement in subclasses"
34
+ end
35
+
36
+ def input
37
+ raise NotImplementedError, "implement in subclasses"
38
+ end
39
+
40
+ def metadata
41
+ raise NotImplementedError, "implement in subclasses"
42
+ end
43
+
44
+ def as_json
45
+ {
46
+ kind: kind,
47
+ blocked: blocked?,
48
+ metadata: metadata,
49
+ operation: @operation
50
+ }.merge(input.as_json)
51
+ end
52
+
53
+ def exception(*)
54
+ raise NotImplementedError, "implement in subclasses"
55
+ end
56
+ end
57
+
58
+ module Attacks
59
+ class PathTraversalAttack < Attack
60
+ attr_reader :input
61
+ attr_reader :filepath
62
+
63
+ def initialize(input:, filepath:, **opts)
64
+ super(**opts)
65
+ @input = input
66
+ @filepath = filepath
67
+ end
68
+
69
+ def metadata
70
+ {filename: filepath}
71
+ end
72
+
73
+ def humanized_name
74
+ "path traversal attack"
75
+ end
76
+
77
+ def kind
78
+ "path_traversal"
79
+ end
80
+
81
+ def exception(*)
82
+ PathTraversalError.new(self)
83
+ end
84
+ end
85
+
86
+ class ShellInjectionAttack < Attack
87
+ attr_reader :input
88
+ attr_reader :command
89
+
90
+ def initialize(input:, command:, **opts)
91
+ super(**opts)
92
+ @input = input
93
+ @command = command
94
+ end
95
+
96
+ def humanized_name
97
+ "shell injection"
98
+ end
99
+
100
+ def kind
101
+ "shell_injection"
102
+ end
103
+
104
+ def metadata
105
+ {
106
+ command: @command
107
+ }
108
+ end
109
+
110
+ def exception(*)
111
+ ShellInjectionError.new(self)
112
+ end
113
+ end
114
+
115
+ class SQLInjectionAttack < Attack
116
+ attr_reader :query
117
+ attr_reader :input
118
+ attr_reader :dialect
119
+
120
+ def initialize(query:, input:, dialect:, **opts)
121
+ super(**opts)
122
+ @query = query
123
+ @input = input
124
+ @dialect = dialect
125
+ end
126
+
127
+ def humanized_name
128
+ "SQL injection"
129
+ end
130
+
131
+ def kind
132
+ "sql_injection"
133
+ end
134
+
135
+ def metadata
136
+ {sql: @query}
137
+ end
138
+
139
+ def exception(*)
140
+ SQLInjectionError.new(self)
141
+ end
142
+ end
143
+
144
+ class SSRFAttack < Attack
145
+ attr_reader :input
146
+ attr_reader :request
147
+
148
+ def initialize(request:, input:, **opts)
149
+ super(**opts)
150
+ @input = input
151
+ @request = request
152
+ end
153
+
154
+ def humanized_name
155
+ "server-side request forgery"
156
+ end
157
+
158
+ def kind
159
+ "ssrf"
160
+ end
161
+
162
+ def exception(*)
163
+ SSRFDetectedError.new(self)
164
+ end
165
+
166
+ def metadata
167
+ {
168
+ host: @request.uri.hostname,
169
+ port: @request.uri.port
170
+ }
171
+ end
172
+ end
173
+
174
+ # Special case of an SSRF attack where we don't have a context—we're just
175
+ # detecting a request to a particularly sensitive address.
176
+ class StoredSSRFAttack < Attack
177
+ attr_reader :hostname
178
+ attr_reader :address
179
+
180
+ def initialize(hostname:, address:, **opts)
181
+ super(**opts)
182
+ @hostname = hostname
183
+ @address = address
184
+ end
185
+
186
+ def humanized_name
187
+ "server-side request forgery"
188
+ end
189
+
190
+ def exception(*)
191
+ SSRFDetectedError.new(self)
192
+ end
193
+
194
+ def kind
195
+ "ssrf"
196
+ end
197
+
198
+ def input
199
+ Aikido::Zen::Payload::UNKNOWN_PAYLOAD
200
+ end
201
+
202
+ def metadata
203
+ {}
204
+ end
205
+ end
206
+ end
207
+ end
@@ -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