aikido-zen 1.0.1.beta.2-x86_64-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.
- checksums.yaml +7 -0
- data/.aikido +6 -0
- data/.ruby-version +1 -0
- data/.simplecov +26 -0
- data/.standard.yml +3 -0
- data/LICENSE +674 -0
- data/README.md +146 -0
- data/Rakefile +67 -0
- data/benchmarks/README.md +23 -0
- data/benchmarks/rails7.1_sql_injection.js +70 -0
- data/docs/banner.svg +202 -0
- data/docs/config.md +125 -0
- data/docs/rails.md +70 -0
- data/lib/aikido/zen/actor.rb +116 -0
- data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
- data/lib/aikido/zen/agent.rb +179 -0
- data/lib/aikido/zen/api_client.rb +142 -0
- data/lib/aikido/zen/attack.rb +207 -0
- data/lib/aikido/zen/background_worker.rb +52 -0
- data/lib/aikido/zen/capped_collections.rb +68 -0
- data/lib/aikido/zen/collector/hosts.rb +15 -0
- data/lib/aikido/zen/collector/routes.rb +66 -0
- data/lib/aikido/zen/collector/sink_stats.rb +95 -0
- data/lib/aikido/zen/collector/stats.rb +111 -0
- data/lib/aikido/zen/collector/users.rb +30 -0
- data/lib/aikido/zen/collector.rb +144 -0
- data/lib/aikido/zen/config.rb +279 -0
- data/lib/aikido/zen/context/rack_request.rb +24 -0
- data/lib/aikido/zen/context/rails_request.rb +42 -0
- data/lib/aikido/zen/context.rb +112 -0
- data/lib/aikido/zen/detached_agent/agent.rb +78 -0
- data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
- data/lib/aikido/zen/detached_agent/server.rb +41 -0
- data/lib/aikido/zen/detached_agent.rb +2 -0
- data/lib/aikido/zen/errors.rb +107 -0
- data/lib/aikido/zen/event.rb +71 -0
- data/lib/aikido/zen/internals.rb +102 -0
- data/lib/aikido/zen/libzen-v0.1.39-x86_64-linux-musl.so +0 -0
- data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
- data/lib/aikido/zen/middleware/middleware.rb +11 -0
- data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
- data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
- data/lib/aikido/zen/middleware/set_context.rb +26 -0
- data/lib/aikido/zen/outbound_connection.rb +45 -0
- data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
- data/lib/aikido/zen/package.rb +22 -0
- data/lib/aikido/zen/payload.rb +50 -0
- data/lib/aikido/zen/rails_engine.rb +70 -0
- data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
- data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
- data/lib/aikido/zen/rate_limiter/result.rb +31 -0
- data/lib/aikido/zen/rate_limiter.rb +50 -0
- data/lib/aikido/zen/request/heuristic_router.rb +115 -0
- data/lib/aikido/zen/request/rails_router.rb +72 -0
- data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
- data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
- data/lib/aikido/zen/request/schema/builder.rb +121 -0
- data/lib/aikido/zen/request/schema/definition.rb +107 -0
- data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
- data/lib/aikido/zen/request/schema.rb +87 -0
- data/lib/aikido/zen/request.rb +103 -0
- data/lib/aikido/zen/route.rb +39 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
- data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
- data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
- data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
- data/lib/aikido/zen/runtime_settings.rb +65 -0
- data/lib/aikido/zen/scan.rb +75 -0
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
- data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -0
- data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
- data/lib/aikido/zen/scanners.rb +7 -0
- data/lib/aikido/zen/sink.rb +118 -0
- data/lib/aikido/zen/sinks/action_controller.rb +83 -0
- data/lib/aikido/zen/sinks/async_http.rb +82 -0
- data/lib/aikido/zen/sinks/curb.rb +115 -0
- data/lib/aikido/zen/sinks/em_http.rb +85 -0
- data/lib/aikido/zen/sinks/excon.rb +121 -0
- data/lib/aikido/zen/sinks/file.rb +116 -0
- data/lib/aikido/zen/sinks/http.rb +95 -0
- data/lib/aikido/zen/sinks/httpclient.rb +97 -0
- data/lib/aikido/zen/sinks/httpx.rb +80 -0
- data/lib/aikido/zen/sinks/kernel.rb +34 -0
- data/lib/aikido/zen/sinks/mysql2.rb +33 -0
- data/lib/aikido/zen/sinks/net_http.rb +103 -0
- data/lib/aikido/zen/sinks/patron.rb +105 -0
- data/lib/aikido/zen/sinks/pg.rb +74 -0
- data/lib/aikido/zen/sinks/resolv.rb +62 -0
- data/lib/aikido/zen/sinks/socket.rb +80 -0
- data/lib/aikido/zen/sinks/sqlite3.rb +49 -0
- data/lib/aikido/zen/sinks/trilogy.rb +33 -0
- data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
- data/lib/aikido/zen/sinks.rb +39 -0
- data/lib/aikido/zen/sinks_dsl.rb +226 -0
- data/lib/aikido/zen/synchronizable.rb +24 -0
- data/lib/aikido/zen/system_info.rb +84 -0
- data/lib/aikido/zen/version.rb +10 -0
- data/lib/aikido/zen/worker.rb +87 -0
- data/lib/aikido/zen.rb +206 -0
- data/lib/aikido-zen.rb +3 -0
- data/placeholder/.gitignore +4 -0
- data/placeholder/README.md +11 -0
- data/placeholder/Rakefile +75 -0
- data/placeholder/lib/placeholder.rb.template +3 -0
- data/placeholder/placeholder.gemspec.template +20 -0
- data/tasklib/bench.rake +94 -0
- data/tasklib/libzen.rake +132 -0
- data/tasklib/wrk.rb +88 -0
- 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"
|