aikido-zen 0.1.0.alpha4-arm64-linux → 0.1.0-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.
- checksums.yaml +4 -4
- data/README.md +136 -23
- data/Rakefile +4 -0
- data/benchmarks/README.md +27 -0
- data/benchmarks/rails7.1_sql_injection.js +74 -0
- data/docs/banner.svg +203 -0
- data/docs/config.md +123 -0
- data/docs/rails.md +70 -0
- data/lib/aikido/zen/actor.rb +1 -1
- data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
- data/lib/aikido/zen/agent.rb +98 -112
- data/lib/aikido/zen/collector/hosts.rb +15 -0
- data/lib/aikido/zen/collector/routes.rb +64 -0
- data/lib/aikido/zen/{stats → collector}/sink_stats.rb +1 -1
- data/lib/aikido/zen/collector/stats.rb +111 -0
- data/lib/aikido/zen/{stats → collector}/users.rb +6 -2
- data/lib/aikido/zen/collector.rb +117 -0
- data/lib/aikido/zen/config.rb +17 -11
- data/lib/aikido/zen/context.rb +8 -1
- data/lib/aikido/zen/errors.rb +3 -1
- data/lib/aikido/zen/event.rb +7 -4
- data/lib/aikido/zen/libzen-v0.1.30.aarch64.so +0 -0
- data/lib/aikido/zen/middleware/set_context.rb +4 -1
- data/lib/aikido/zen/rails_engine.rb +27 -18
- data/lib/aikido/zen/request/schema/builder.rb +0 -2
- data/lib/aikido/zen/request.rb +6 -0
- data/lib/aikido/zen/runtime_settings.rb +6 -11
- data/lib/aikido/zen/sinks/action_controller.rb +64 -0
- data/lib/aikido/zen/sinks.rb +1 -0
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen/worker.rb +82 -0
- data/lib/aikido/zen.rb +55 -50
- data/tasklib/bench.rake +70 -0
- metadata +19 -9
- data/CODE_OF_CONDUCT.md +0 -132
- data/lib/aikido/zen/libzen-v0.1.26.aarch64.so +0 -0
- data/lib/aikido/zen/stats/routes.rb +0 -53
- 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
|
data/lib/aikido/zen/agent.rb
CHANGED
@@ -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,87 @@ 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
|
-
#
|
14
|
-
|
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
|
-
|
20
|
-
|
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
|
-
@
|
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) { send_heartbeat if stats.any? }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Clean up any ongoing threads, and reset the state. Called automatically
|
72
|
+
# when the process exits.
|
73
|
+
#
|
74
|
+
# @return [void]
|
75
|
+
def stop!
|
76
|
+
@config.logger.info "Stopping Aikido agent"
|
77
|
+
@started_at = nil
|
78
|
+
@worker.shutdown
|
79
|
+
end
|
80
|
+
|
81
|
+
# Respond to the runtime settings changing after being fetched from the
|
82
|
+
# Aikido servers.
|
83
|
+
#
|
84
|
+
# @return [void]
|
85
|
+
def updated_settings!
|
86
|
+
if !heartbeats.running?
|
87
|
+
heartbeats.start { send_heartbeat }
|
88
|
+
elsif heartbeats.stale_settings?
|
89
|
+
heartbeats.restart { send_heartbeat }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
38
93
|
# Given an Attack, report it to the Aikido server, and/or block the request
|
39
94
|
# depending on configuration.
|
40
95
|
#
|
@@ -49,8 +104,8 @@ module Aikido::Zen
|
|
49
104
|
@config.logger.error("[ATTACK DETECTED] #{attack.log_message}")
|
50
105
|
report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
|
51
106
|
|
52
|
-
|
53
|
-
raise attack if
|
107
|
+
@collector.track_attack(attack)
|
108
|
+
raise attack if attack.blocked?
|
54
109
|
end
|
55
110
|
|
56
111
|
# Asynchronously reports an Event of any kind to the Aikido dashboard. If
|
@@ -62,7 +117,7 @@ module Aikido::Zen
|
|
62
117
|
#
|
63
118
|
# @return [void]
|
64
119
|
def report(event)
|
65
|
-
|
120
|
+
@worker.perform do
|
66
121
|
response = @api_client.report(event)
|
67
122
|
yield response if response && block_given?
|
68
123
|
rescue Aikido::Zen::APIError, Aikido::Zen::NetworkError => err
|
@@ -70,53 +125,35 @@ module Aikido::Zen
|
|
70
125
|
end
|
71
126
|
end
|
72
127
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
128
|
+
# @api private
|
129
|
+
#
|
130
|
+
# Atomically flushes all the stats stored by the agent, and sends a
|
131
|
+
# heartbeat event. Scheduled to run automatically on a recurring schedule
|
132
|
+
# when reporting is enabled.
|
133
|
+
#
|
134
|
+
# @param at [Time] the event time. Defaults to now.
|
135
|
+
# @return [void]
|
136
|
+
# @see Aikido::Zen::RuntimeSettings#heartbeat_interval
|
137
|
+
def send_heartbeat(at: Time.now.utc)
|
109
138
|
return unless @api_client.can_make_requests?
|
110
139
|
|
111
|
-
|
112
|
-
|
140
|
+
event = @collector.flush(at: at)
|
141
|
+
|
142
|
+
report(event) do |response|
|
113
143
|
Aikido::Zen.runtime_settings.update_from_json(response)
|
114
144
|
@config.logger.info "Updated runtime settings after heartbeat"
|
115
145
|
end
|
116
146
|
end
|
117
147
|
|
148
|
+
# @api private
|
149
|
+
#
|
150
|
+
# Sets up the timer task that polls the Aikido Runtime API for updates to
|
151
|
+
# the runtime settings every minute.
|
152
|
+
#
|
153
|
+
# @return [void]
|
154
|
+
# @see Aikido::Zen::RuntimeSettings
|
118
155
|
def poll_for_setting_updates
|
119
|
-
|
156
|
+
@worker.every(@config.polling_interval) do
|
120
157
|
if @api_client.should_fetch_settings?
|
121
158
|
Aikido::Zen.runtime_settings.update_from_json(@api_client.fetch_settings)
|
122
159
|
@config.logger.info "Updated runtime settings after polling"
|
@@ -124,64 +161,13 @@ module Aikido::Zen
|
|
124
161
|
end
|
125
162
|
end
|
126
163
|
|
127
|
-
def
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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 }
|
164
|
+
private def heartbeats
|
165
|
+
@heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
|
166
|
+
config: @config,
|
167
|
+
worker: @worker
|
168
|
+
)
|
185
169
|
end
|
186
170
|
end
|
187
171
|
end
|
172
|
+
|
173
|
+
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
|
@@ -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
|
-
|
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"
|