aikido-zen 0.1.0.alpha4-arm64-darwin
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/.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.dylib +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
|