aikido-zen 0.1.0.alpha4-arm64-linux
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +674 -0
- data/README.md +40 -0
- data/Rakefile +63 -0
- data/lib/aikido/zen/actor.rb +116 -0
- data/lib/aikido/zen/agent.rb +187 -0
- data/lib/aikido/zen/api_client.rb +132 -0
- data/lib/aikido/zen/attack.rb +138 -0
- data/lib/aikido/zen/capped_collections.rb +68 -0
- data/lib/aikido/zen/config.rb +229 -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 +101 -0
- data/lib/aikido/zen/errors.rb +88 -0
- data/lib/aikido/zen/event.rb +66 -0
- data/lib/aikido/zen/internals.rb +64 -0
- data/lib/aikido/zen/libzen-v0.1.26.aarch64.so +0 -0
- data/lib/aikido/zen/middleware/check_allowed_addresses.rb +38 -0
- data/lib/aikido/zen/middleware/set_context.rb +26 -0
- data/lib/aikido/zen/middleware/throttler.rb +50 -0
- data/lib/aikido/zen/outbound_connection.rb +45 -0
- data/lib/aikido/zen/outbound_connection_monitor.rb +19 -0
- data/lib/aikido/zen/package.rb +22 -0
- data/lib/aikido/zen/payload.rb +48 -0
- data/lib/aikido/zen/rails_engine.rb +53 -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 +55 -0
- data/lib/aikido/zen/request/heuristic_router.rb +109 -0
- data/lib/aikido/zen/request/rails_router.rb +84 -0
- data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
- data/lib/aikido/zen/request/schema/auth_schemas.rb +40 -0
- data/lib/aikido/zen/request/schema/builder.rb +125 -0
- data/lib/aikido/zen/request/schema/definition.rb +112 -0
- data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
- data/lib/aikido/zen/request/schema.rb +72 -0
- data/lib/aikido/zen/request.rb +97 -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 +70 -0
- data/lib/aikido/zen/scan.rb +75 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +95 -0
- data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +85 -0
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +251 -0
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +43 -0
- data/lib/aikido/zen/scanners.rb +5 -0
- data/lib/aikido/zen/sink.rb +108 -0
- data/lib/aikido/zen/sinks/async_http.rb +63 -0
- data/lib/aikido/zen/sinks/curb.rb +89 -0
- data/lib/aikido/zen/sinks/em_http.rb +71 -0
- data/lib/aikido/zen/sinks/excon.rb +103 -0
- data/lib/aikido/zen/sinks/http.rb +76 -0
- data/lib/aikido/zen/sinks/httpclient.rb +68 -0
- data/lib/aikido/zen/sinks/httpx.rb +61 -0
- data/lib/aikido/zen/sinks/mysql2.rb +21 -0
- data/lib/aikido/zen/sinks/net_http.rb +85 -0
- data/lib/aikido/zen/sinks/patron.rb +88 -0
- data/lib/aikido/zen/sinks/pg.rb +50 -0
- data/lib/aikido/zen/sinks/resolv.rb +41 -0
- data/lib/aikido/zen/sinks/socket.rb +51 -0
- data/lib/aikido/zen/sinks/sqlite3.rb +30 -0
- data/lib/aikido/zen/sinks/trilogy.rb +21 -0
- data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
- data/lib/aikido/zen/sinks.rb +21 -0
- data/lib/aikido/zen/stats/routes.rb +53 -0
- data/lib/aikido/zen/stats/sink_stats.rb +95 -0
- data/lib/aikido/zen/stats/users.rb +26 -0
- data/lib/aikido/zen/stats.rb +171 -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.rb +138 -0
- data/lib/aikido-zen.rb +3 -0
- data/lib/aikido.rb +3 -0
- data/tasklib/libzen.rake +128 -0
- metadata +175 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../sink"
|
4
|
+
|
5
|
+
module Aikido::Zen
|
6
|
+
module Sinks
|
7
|
+
module Trilogy
|
8
|
+
SINK = Sinks.add("trilogy", scanners: [Scanners::SQLInjectionScanner])
|
9
|
+
|
10
|
+
module Extensions
|
11
|
+
def query(query, *)
|
12
|
+
SINK.scan(query: query, dialect: :mysql, operation: "query")
|
13
|
+
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
::Trilogy.prepend(Aikido::Zen::Sinks::Trilogy::Extensions)
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../sink"
|
4
|
+
require_relative "../outbound_connection_monitor"
|
5
|
+
|
6
|
+
module Aikido::Zen
|
7
|
+
module Sinks
|
8
|
+
module Typhoeus
|
9
|
+
SINK = Sinks.add("typhoeus", scanners: [
|
10
|
+
Aikido::Zen::Scanners::SSRFScanner,
|
11
|
+
Aikido::Zen::OutboundConnectionMonitor
|
12
|
+
])
|
13
|
+
|
14
|
+
before_callback = ->(request) {
|
15
|
+
wrapped_request = Aikido::Zen::Scanners::SSRFScanner::Request.new(
|
16
|
+
verb: request.options[:method],
|
17
|
+
uri: URI(request.url),
|
18
|
+
headers: request.options[:headers]
|
19
|
+
)
|
20
|
+
|
21
|
+
# Store the request information so the DNS sinks can pick it up.
|
22
|
+
if (context = Aikido::Zen.current_context)
|
23
|
+
prev_request = context["ssrf.request"]
|
24
|
+
context["ssrf.request"] = wrapped_request
|
25
|
+
end
|
26
|
+
|
27
|
+
SINK.scan(
|
28
|
+
connection: Aikido::Zen::OutboundConnection.from_uri(URI(request.base_url)),
|
29
|
+
request: wrapped_request,
|
30
|
+
operation: "request"
|
31
|
+
)
|
32
|
+
|
33
|
+
request.on_headers do |response|
|
34
|
+
context["ssrf.request"] = prev_request if context
|
35
|
+
|
36
|
+
Aikido::Zen::Scanners::SSRFScanner.track_redirects(
|
37
|
+
request: wrapped_request,
|
38
|
+
response: Aikido::Zen::Scanners::SSRFScanner::Response.new(
|
39
|
+
status: response.code,
|
40
|
+
headers: response.headers.to_h
|
41
|
+
)
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
# When Typhoeus is configured with followlocation: true, the redirect
|
46
|
+
# following happens between the on_headers and the on_complete callback,
|
47
|
+
# so we need this one to detect if the request resulted in an automatic
|
48
|
+
# redirect that was followed.
|
49
|
+
request.on_complete do |response|
|
50
|
+
break if response.effective_url == request.url
|
51
|
+
|
52
|
+
last_effective_request = Aikido::Zen::Scanners::SSRFScanner::Request.new(
|
53
|
+
verb: request.options[:method],
|
54
|
+
uri: URI(response.effective_url),
|
55
|
+
headers: request.options[:headers]
|
56
|
+
)
|
57
|
+
context["ssrf.request"] = last_effective_request if context
|
58
|
+
|
59
|
+
# In this case, we can't actually stop the request from happening, but
|
60
|
+
# we can scan again (now that we know another request happened), to
|
61
|
+
# stop the response from being exposed to the user. This downgrades
|
62
|
+
# the SSRF into a blind SSRF, which is better than doing nothing.
|
63
|
+
SINK.scan(
|
64
|
+
connection: Aikido::Zen::OutboundConnection.from_uri(URI(response.effective_url)),
|
65
|
+
request: last_effective_request,
|
66
|
+
operation: "request"
|
67
|
+
)
|
68
|
+
ensure
|
69
|
+
context["ssrf.request"] = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
true
|
73
|
+
}
|
74
|
+
|
75
|
+
::Typhoeus.before.prepend(before_callback)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "sink"
|
4
|
+
|
5
|
+
require_relative "sinks/socket"
|
6
|
+
|
7
|
+
require_relative "sinks/resolv" if defined?(::Resolv)
|
8
|
+
require_relative "sinks/net_http" if defined?(::Net::HTTP)
|
9
|
+
require_relative "sinks/http" if defined?(::HTTP)
|
10
|
+
require_relative "sinks/httpx" if defined?(::HTTPX)
|
11
|
+
require_relative "sinks/httpclient" if defined?(::HTTPClient)
|
12
|
+
require_relative "sinks/excon" if defined?(::Excon)
|
13
|
+
require_relative "sinks/curb" if defined?(::Curl)
|
14
|
+
require_relative "sinks/patron" if defined?(::Patron)
|
15
|
+
require_relative "sinks/typhoeus" if defined?(::Typhoeus)
|
16
|
+
require_relative "sinks/async_http" if defined?(::Async::HTTP)
|
17
|
+
require_relative "sinks/em_http" if defined?(::EventMachine::HttpRequest)
|
18
|
+
require_relative "sinks/mysql2" if defined?(::Mysql2)
|
19
|
+
require_relative "sinks/pg" if defined?(::PG)
|
20
|
+
require_relative "sinks/sqlite3" if defined?(::SQLite3)
|
21
|
+
require_relative "sinks/trilogy" if defined?(::Trilogy)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../request/schema/empty_schema"
|
4
|
+
|
5
|
+
class Aikido::Zen::Stats
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
# Keeps track of the visited routes.
|
9
|
+
class Routes
|
10
|
+
def initialize
|
11
|
+
@routes = Hash.new do |h, k|
|
12
|
+
h[k] = Record.new(0, Aikido::Zen::Request::Schema::EMPTY_SCHEMA)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param route [Aikido::Zen::Route, nil] tracks the visit, if given.
|
17
|
+
# @param schema [Aikido::Zen::Request::Schema, nil] the schema of the
|
18
|
+
# request, if the feature is enabled.
|
19
|
+
# @return [void]
|
20
|
+
def add(route, schema = nil)
|
21
|
+
@routes[route].add(schema) if route
|
22
|
+
end
|
23
|
+
|
24
|
+
# @!visibility private
|
25
|
+
def [](route)
|
26
|
+
@routes[route]
|
27
|
+
end
|
28
|
+
|
29
|
+
# @!visibility private
|
30
|
+
def empty?
|
31
|
+
@routes.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def as_json
|
35
|
+
@routes.map do |route, record|
|
36
|
+
{
|
37
|
+
method: route.verb,
|
38
|
+
path: route.path,
|
39
|
+
hits: record.hits,
|
40
|
+
apispec: record.schema&.as_json
|
41
|
+
}.compact
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @!visibility private
|
46
|
+
Record = Struct.new(:hits, :schema) do
|
47
|
+
def add(new_schema = nil)
|
48
|
+
self.hits += 1
|
49
|
+
self.schema |= new_schema
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
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 Stats::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,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../capped_collections"
|
4
|
+
|
5
|
+
class Aikido::Zen::Stats
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
# Keeps track of the users that were seen by the app.
|
9
|
+
class Users < Aikido::Zen::CappedMap
|
10
|
+
def add(actor)
|
11
|
+
if key?(actor.id)
|
12
|
+
self[actor.id].update
|
13
|
+
else
|
14
|
+
self[actor.id] = actor
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def each(&b)
|
19
|
+
each_value(&b)
|
20
|
+
end
|
21
|
+
|
22
|
+
def as_json
|
23
|
+
map(&:as_json)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent"
|
4
|
+
|
5
|
+
require_relative "capped_collections"
|
6
|
+
|
7
|
+
module Aikido::Zen
|
8
|
+
# Tracks information about how the Aikido Agent is used in the app.
|
9
|
+
class Stats < Concurrent::Synchronization::LockableObject
|
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
|
+
reset_state
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Boolean]
|
23
|
+
def empty?
|
24
|
+
synchronize { @requests.zero? && @sinks.empty? }
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Boolean]
|
28
|
+
def any?
|
29
|
+
!empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Track the timestamp we start tracking this series of stats.
|
33
|
+
#
|
34
|
+
# @return [void]
|
35
|
+
def start(at = Time.now.utc)
|
36
|
+
synchronize { @started_at = at }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Atomically copies the stats and resets them to their initial values, so
|
40
|
+
# you can start gathering a new set while being able to read the old ones
|
41
|
+
# without fear that a thread might modify them.
|
42
|
+
#
|
43
|
+
# @param at [Time] the time at which we're resetting, which is set as the
|
44
|
+
# ending time for the returned copy.
|
45
|
+
#
|
46
|
+
# @return [Aikido::Zen::Stats] a frozen copy of the object before
|
47
|
+
# resetting, so you can access the data there, with its +ended_at+
|
48
|
+
# set to the given timestamp.
|
49
|
+
def reset(at: Time.now.utc)
|
50
|
+
synchronize {
|
51
|
+
# Make sure the timing stats are compressed before copying, since we
|
52
|
+
# need these compressed when we serialize this for the API.
|
53
|
+
@sinks.each_value(&:compress_timings)
|
54
|
+
|
55
|
+
copy = clone
|
56
|
+
copy.ended_at = at
|
57
|
+
|
58
|
+
reset_state
|
59
|
+
start(at)
|
60
|
+
|
61
|
+
copy
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param request [Aikido::Zen::Request]
|
66
|
+
# @return [void]
|
67
|
+
def add_request(request)
|
68
|
+
synchronize {
|
69
|
+
@requests += 1
|
70
|
+
@routes.add(request.route, request.schema)
|
71
|
+
}
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# @param connection [Aikido::Zen::OutboundConnection]
|
76
|
+
# @return [void]
|
77
|
+
def add_outbound(connection)
|
78
|
+
synchronize { @outbound_connections << connection }
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# @param [Aikido::Zen::Scan]
|
83
|
+
# @return [void]
|
84
|
+
def add_scan(scan)
|
85
|
+
synchronize {
|
86
|
+
stats = @sinks[scan.sink.name]
|
87
|
+
stats.scans += 1
|
88
|
+
stats.errors += 1 if scan.errors?
|
89
|
+
stats.add_timing(scan.duration)
|
90
|
+
}
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
# @param attack [Aikido::Zen::Attack]
|
95
|
+
# @param being_blocked [Boolean] whether the Agent blocked the
|
96
|
+
# request where this Attack happened or not.
|
97
|
+
#
|
98
|
+
# @return [void]
|
99
|
+
def add_attack(attack, being_blocked:)
|
100
|
+
synchronize {
|
101
|
+
stats = @sinks[attack.sink.name]
|
102
|
+
stats.attacks += 1
|
103
|
+
stats.blocked_attacks += 1 if being_blocked
|
104
|
+
}
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
# @param actor [Aikido::Zen::Actor]
|
109
|
+
# @return [void]
|
110
|
+
def add_user(actor)
|
111
|
+
synchronize { @users.add(actor) }
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [#as_json] the set of routes visited during this stats-gathering
|
115
|
+
# period.
|
116
|
+
def routes
|
117
|
+
synchronize { @routes }
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return [Enumerable<#as_json>] the set of users that had an active session
|
121
|
+
# during this stats-gathering period.
|
122
|
+
def users
|
123
|
+
synchronize { @users }
|
124
|
+
end
|
125
|
+
|
126
|
+
# @return [#as_json] the set of connections to outbound hosts that were
|
127
|
+
# established during this stats-gathering period.
|
128
|
+
def outbound_connections
|
129
|
+
synchronize { @outbound_connections }
|
130
|
+
end
|
131
|
+
|
132
|
+
def as_json
|
133
|
+
synchronize {
|
134
|
+
total_attacks, total_blocked = aggregate_attacks_from_sinks
|
135
|
+
{
|
136
|
+
startedAt: @started_at.to_i * 1000,
|
137
|
+
endedAt: (@ended_at.to_i * 1000 if @ended_at),
|
138
|
+
sinks: @sinks.transform_values(&:as_json),
|
139
|
+
requests: {
|
140
|
+
total: @requests,
|
141
|
+
aborted: @aborted_requests,
|
142
|
+
attacksDetected: {
|
143
|
+
total: total_attacks,
|
144
|
+
blocked: total_blocked
|
145
|
+
}
|
146
|
+
}
|
147
|
+
}
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
private def reset_state
|
152
|
+
@sinks = Hash.new { |h, k| h[k] = SinkStats.new(k, @config) }
|
153
|
+
@started_at = @ended_at = nil
|
154
|
+
@requests = 0
|
155
|
+
@routes = Routes.new
|
156
|
+
@users = Users.new(@config.max_users_tracked)
|
157
|
+
@outbound_connections = CappedSet.new(@config.max_outbound_connections)
|
158
|
+
@aborted_requests = 0
|
159
|
+
end
|
160
|
+
|
161
|
+
private def aggregate_attacks_from_sinks
|
162
|
+
@sinks.each_value.reduce([0, 0]) { |(attacks, blocked), stats|
|
163
|
+
[attacks + stats.attacks, blocked + stats.blocked_attacks]
|
164
|
+
}
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
require_relative "stats/users"
|
170
|
+
require_relative "stats/routes"
|
171
|
+
require_relative "stats/sink_stats"
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen
|
4
|
+
# @!visibility private
|
5
|
+
#
|
6
|
+
# Provides the synchronization part of Concurrent's LockableObject, but allows
|
7
|
+
# objects to take keyword arguments as well.
|
8
|
+
#
|
9
|
+
# NOTE: This is meant to be prepennded.
|
10
|
+
module Synchronizable
|
11
|
+
def initialize(*, **)
|
12
|
+
@__lock__ = ::Mutex.new
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def synchronize
|
17
|
+
if @__lock__.owned?
|
18
|
+
yield
|
19
|
+
else
|
20
|
+
@__lock__.synchronize { yield }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
require "rubygems"
|
5
|
+
|
6
|
+
require_relative "package"
|
7
|
+
|
8
|
+
module Aikido::Zen
|
9
|
+
# Provides information about the currently running Agent.
|
10
|
+
class SystemInfo
|
11
|
+
def initialize(config = Aikido::Zen.config)
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def attacks_block_requests?
|
16
|
+
!!@config.blocking_mode
|
17
|
+
end
|
18
|
+
|
19
|
+
def attacks_are_only_reported?
|
20
|
+
!attacks_block_requests?
|
21
|
+
end
|
22
|
+
|
23
|
+
def library_name
|
24
|
+
"firewall-ruby"
|
25
|
+
end
|
26
|
+
|
27
|
+
def library_version
|
28
|
+
VERSION
|
29
|
+
end
|
30
|
+
|
31
|
+
def platform_version
|
32
|
+
RUBY_VERSION
|
33
|
+
end
|
34
|
+
|
35
|
+
def hostname
|
36
|
+
@hostname ||= Socket.gethostname
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Array<Aikido::Zen::Package>] a list of loaded rubygems that are
|
40
|
+
# supported by Aikido (i.e. we have a Sink that scans the package for
|
41
|
+
# vulnerabilities and protects you).
|
42
|
+
def packages
|
43
|
+
@packages ||= Gem.loaded_specs
|
44
|
+
.map { |_, spec| Package.new(spec.name, spec.version) }
|
45
|
+
.select(&:supported?)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [String] the first non-loopback IPv4 address that we can use
|
49
|
+
# to identify this host. If the machine is solely identified by IPv6
|
50
|
+
# addresses, then this will instead return an IPv6 address.
|
51
|
+
def ip_address
|
52
|
+
@ip_address ||= Socket.ip_address_list
|
53
|
+
.reject { |ip| ip.ipv4_loopback? || ip.ipv6_loopback? || ip.unix? }
|
54
|
+
.min_by { |ip| ip.ipv4? ? 0 : 1 }
|
55
|
+
.ip_address
|
56
|
+
end
|
57
|
+
|
58
|
+
def os_name
|
59
|
+
Gem::Platform.local.os
|
60
|
+
end
|
61
|
+
|
62
|
+
def os_version
|
63
|
+
Gem::Platform.local.version
|
64
|
+
end
|
65
|
+
|
66
|
+
def as_json
|
67
|
+
{
|
68
|
+
dryMode: attacks_are_only_reported?,
|
69
|
+
library: library_name,
|
70
|
+
version: library_version,
|
71
|
+
hostname: hostname,
|
72
|
+
ipAddress: ip_address,
|
73
|
+
platform: {version: platform_version},
|
74
|
+
os: {name: os_name, version: os_version},
|
75
|
+
packages: packages.reduce({}) { |all, package| all.update(package.as_json) },
|
76
|
+
incompatiblePackages: {},
|
77
|
+
stack: [],
|
78
|
+
serverless: false,
|
79
|
+
nodeEnv: "",
|
80
|
+
preventedPrototypePollution: false
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|