aikido-zen 0.1.0.alpha4-x86_64-mingw-64 → 0.1.0-x86_64-mingw-64
Sign up to get free protection for your applications and to get access to all the features.
- 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.26.x86_64.dll → libzen-v0.1.30.x86_64.dll} +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/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"
|