aikido-zen 1.1.0 → 1.1.2
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/docs/rails.md +1 -1
- data/lib/aikido/zen/actor.rb +2 -2
- data/lib/aikido/zen/agent.rb +3 -0
- data/lib/aikido/zen/attack_wave/helpers.rb +1 -1
- data/lib/aikido/zen/attack_wave.rb +69 -7
- data/lib/aikido/zen/cache.rb +8 -2
- data/lib/aikido/zen/collector/event.rb +44 -1
- data/lib/aikido/zen/collector/stats.rb +18 -1
- data/lib/aikido/zen/collector.rb +27 -2
- data/lib/aikido/zen/config.rb +43 -9
- data/lib/aikido/zen/current_context.rb +27 -0
- data/lib/aikido/zen/detached_agent/agent.rb +2 -2
- data/lib/aikido/zen/detached_agent/front_object.rb +1 -1
- data/lib/aikido/zen/detached_agent/server.rb +2 -2
- data/lib/aikido/zen/errors.rb +6 -0
- data/lib/aikido/zen/event.rb +0 -17
- data/lib/aikido/zen/middleware/allowed_address_checker.rb +12 -4
- data/lib/aikido/zen/middleware/attack_protector.rb +2 -3
- data/lib/aikido/zen/middleware/attack_wave_protector.rb +21 -3
- data/lib/aikido/zen/middleware/ip_list_checker.rb +47 -0
- data/lib/aikido/zen/middleware/rack_throttler.rb +6 -2
- data/lib/aikido/zen/middleware/request_tracker.rb +2 -3
- data/lib/aikido/zen/middleware/user_agent_checker.rb +1 -2
- data/lib/aikido/zen/outbound_connection.rb +1 -1
- data/lib/aikido/zen/payload.rb +1 -1
- data/lib/aikido/zen/rails_engine.rb +1 -0
- data/lib/aikido/zen/rate_limiter/bucket.rb +21 -3
- data/lib/aikido/zen/rate_limiter.rb +22 -15
- data/lib/aikido/zen/request/schema/auth_schemas.rb +8 -7
- data/lib/aikido/zen/request/schema/definition.rb +32 -2
- data/lib/aikido/zen/request/schema.rb +23 -7
- data/lib/aikido/zen/runtime_settings/domain_settings.rb +27 -0
- data/lib/aikido/zen/runtime_settings/domains.rb +41 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +65 -14
- data/lib/aikido/zen/runtime_settings/ip_list.rb +34 -0
- data/lib/aikido/zen/runtime_settings/ip_set.rb +21 -1
- data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +11 -0
- data/lib/aikido/zen/runtime_settings.rb +92 -7
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +17 -4
- data/lib/aikido/zen/scanners/shell_injection/helpers.rb +2 -2
- data/lib/aikido/zen/sinks/action_controller.rb +6 -2
- data/lib/aikido/zen/sinks/async_http.rb +15 -3
- data/lib/aikido/zen/sinks/curb.rb +15 -3
- data/lib/aikido/zen/sinks/em_http.rb +15 -3
- data/lib/aikido/zen/sinks/excon.rb +15 -3
- data/lib/aikido/zen/sinks/http.rb +15 -3
- data/lib/aikido/zen/sinks/httpclient.rb +15 -3
- data/lib/aikido/zen/sinks/httpx.rb +15 -3
- data/lib/aikido/zen/sinks/net_http.rb +29 -9
- data/lib/aikido/zen/sinks/patron.rb +15 -3
- data/lib/aikido/zen/sinks/typhoeus.rb +19 -5
- data/lib/aikido/zen/sinks.rb +5 -0
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen.rb +20 -6
- metadata +21 -3
- data/lib/aikido/zen/outbound_connection_monitor.rb +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 22a0e7e94d5a020957f061bf7139e15f67d599a8a479ebed19d41b3b53893b44
|
|
4
|
+
data.tar.gz: 249cd01e1fb339ff5cfb87dcb6944247e7ee31e990643ca815f1bdcae34a1961
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d18a0dba5e3a68b979f6a8f628f9cdac035786d9abd05fc917c45ffd391c7e9a82d8c844a738c933865d28133fedd74e7696addbd30901a2831c1c7680445a26
|
|
7
|
+
data.tar.gz: ecf92bcb0a52fb3949abccd325d32b81faad3053ee2e92686be5d3315c1b1e157a2fb2ae7cbce68017ca77045acb66edacb4f96e3dfd4ce928c4f3db5cdfc9f7
|
data/docs/rails.md
CHANGED
data/lib/aikido/zen/actor.rb
CHANGED
|
@@ -65,7 +65,7 @@ module Aikido::Zen
|
|
|
65
65
|
def initialize(
|
|
66
66
|
id:,
|
|
67
67
|
name: nil,
|
|
68
|
-
ip: Aikido::Zen.current_context&.request&.
|
|
68
|
+
ip: Aikido::Zen.current_context&.request&.client_ip,
|
|
69
69
|
first_seen_at: Time.now.utc,
|
|
70
70
|
last_seen_at: first_seen_at
|
|
71
71
|
)
|
|
@@ -96,7 +96,7 @@ module Aikido::Zen
|
|
|
96
96
|
# always keep the most recent time if this conflicts with the current
|
|
97
97
|
# value.
|
|
98
98
|
# @return [void]
|
|
99
|
-
def update(seen_at: Time.now.utc, ip: Aikido::Zen.current_context&.request&.
|
|
99
|
+
def update(seen_at: Time.now.utc, ip: Aikido::Zen.current_context&.request&.client_ip)
|
|
100
100
|
@last_seen_at.try_update { |last_seen_at| [last_seen_at, seen_at].max }
|
|
101
101
|
@ip.try_update { |last_ip| [ip, last_ip].compact.first }
|
|
102
102
|
end
|
data/lib/aikido/zen/agent.rb
CHANGED
|
@@ -160,6 +160,9 @@ module Aikido::Zen
|
|
|
160
160
|
if Aikido::Zen.runtime_settings.update_from_runtime_config_json(response)
|
|
161
161
|
updated_settings!
|
|
162
162
|
@config.logger.info("Updated runtime settings after heartbeat")
|
|
163
|
+
|
|
164
|
+
Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(@api_client.fetch_runtime_firewall_lists)
|
|
165
|
+
@config.logger.info("Updated runtime firewall list after heartbeat")
|
|
163
166
|
end
|
|
164
167
|
end
|
|
165
168
|
end
|
|
@@ -6,12 +6,19 @@ require_relative "attack_wave/helpers"
|
|
|
6
6
|
module Aikido::Zen
|
|
7
7
|
module AttackWave
|
|
8
8
|
class Detector
|
|
9
|
+
# @return [Aikido::Zen::CappedSet]
|
|
10
|
+
attr_reader :samples
|
|
11
|
+
|
|
9
12
|
def initialize(config: Aikido::Zen.config, clock: nil)
|
|
10
13
|
@config = config
|
|
11
14
|
|
|
12
15
|
@event_times = Cache.new(@config.attack_wave_max_cache_entries, ttl: @config.attack_wave_min_time_between_events, clock: clock)
|
|
13
16
|
|
|
14
17
|
@request_counts = Cache.new(@config.attack_wave_max_cache_entries, 0, ttl: @config.attack_wave_min_time_between_requests, clock: clock)
|
|
18
|
+
|
|
19
|
+
@samples = Cache.new(@config.attack_wave_max_cache_entries, ttl: @config.attack_wave_min_time_between_requests, clock: clock) do
|
|
20
|
+
CappedSet.new(@config.attack_wave_max_cache_samples)
|
|
21
|
+
end
|
|
15
22
|
end
|
|
16
23
|
|
|
17
24
|
def attack_wave?(context)
|
|
@@ -25,6 +32,13 @@ module Aikido::Zen
|
|
|
25
32
|
|
|
26
33
|
request_count = @request_counts[client_ip] += 1
|
|
27
34
|
|
|
35
|
+
context.request.then do |request|
|
|
36
|
+
@samples[client_ip] <<= Sample.new(
|
|
37
|
+
verb: request.request_method,
|
|
38
|
+
path: request.fullpath
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
28
42
|
return false if request_count < @config.attack_wave_threshold
|
|
29
43
|
|
|
30
44
|
@event_times[client_ip] = Time.now.utc
|
|
@@ -60,29 +74,77 @@ module Aikido::Zen
|
|
|
60
74
|
source: @source
|
|
61
75
|
}.compact
|
|
62
76
|
end
|
|
77
|
+
|
|
78
|
+
def ==(other)
|
|
79
|
+
other.is_a?(self.class) &&
|
|
80
|
+
other.ip_address == ip_address &&
|
|
81
|
+
other.user_agent == user_agent &&
|
|
82
|
+
other.source == source
|
|
83
|
+
end
|
|
84
|
+
alias_method :eql?, :==
|
|
63
85
|
end
|
|
64
86
|
|
|
65
87
|
class Attack
|
|
66
|
-
# @return [
|
|
67
|
-
attr_reader :
|
|
88
|
+
# @return [Aikido::Zen::AttackWave::Sample]
|
|
89
|
+
attr_reader :samples
|
|
68
90
|
|
|
69
91
|
# @return [Aikido::Zen::Actor]
|
|
70
92
|
attr_reader :user
|
|
71
93
|
|
|
72
|
-
# @param
|
|
73
|
-
# @param
|
|
94
|
+
# @param samples [Aikido::Zen::AttackWave::Sample]
|
|
95
|
+
# @param user [Aikido::Zen::Actor]
|
|
74
96
|
# @return [Aikido::Zen::AttackWave::Attack]
|
|
75
|
-
def initialize(
|
|
76
|
-
@
|
|
97
|
+
def initialize(samples:, user:)
|
|
98
|
+
@samples = samples
|
|
77
99
|
@user = user
|
|
78
100
|
end
|
|
79
101
|
|
|
80
102
|
def as_json
|
|
81
103
|
{
|
|
82
|
-
metadata:
|
|
104
|
+
metadata: {
|
|
105
|
+
samples: @samples.as_json.to_json # The API only accepts string values in metadata
|
|
106
|
+
},
|
|
83
107
|
user: @user.as_json
|
|
84
108
|
}.compact
|
|
85
109
|
end
|
|
110
|
+
|
|
111
|
+
def ==(other)
|
|
112
|
+
other.is_a?(self.class) &&
|
|
113
|
+
other.samples == samples &&
|
|
114
|
+
other.user == user
|
|
115
|
+
end
|
|
116
|
+
alias_method :eql?, :==
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class Sample
|
|
120
|
+
# @return [String]
|
|
121
|
+
attr_reader :verb
|
|
122
|
+
|
|
123
|
+
# @return [String]
|
|
124
|
+
attr_reader :path
|
|
125
|
+
|
|
126
|
+
def initialize(verb:, path:)
|
|
127
|
+
@verb = verb
|
|
128
|
+
@path = path
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def as_json
|
|
132
|
+
{
|
|
133
|
+
method: @verb.as_json,
|
|
134
|
+
url: @path.as_json
|
|
135
|
+
}.compact
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def ==(other)
|
|
139
|
+
other.is_a?(self.class) &&
|
|
140
|
+
other.verb == verb &&
|
|
141
|
+
other.path == path
|
|
142
|
+
end
|
|
143
|
+
alias_method :eql?, :==
|
|
144
|
+
|
|
145
|
+
def hash
|
|
146
|
+
[verb, path].hash
|
|
147
|
+
end
|
|
86
148
|
end
|
|
87
149
|
end
|
|
88
150
|
end
|
data/lib/aikido/zen/cache.rb
CHANGED
|
@@ -9,11 +9,13 @@ module Aikido::Zen
|
|
|
9
9
|
def_delegators :@data,
|
|
10
10
|
:size, :empty?
|
|
11
11
|
|
|
12
|
-
def initialize(capacity, default_value = nil, ttl:, clock: nil)
|
|
12
|
+
def initialize(capacity, default_value = nil, ttl:, clock: nil, &block)
|
|
13
13
|
@default_value = default_value
|
|
14
14
|
@ttl = ttl
|
|
15
15
|
@clock = clock
|
|
16
16
|
|
|
17
|
+
@initialize_block = block
|
|
18
|
+
|
|
17
19
|
@data = CappedMap.new(capacity, mode: :lru)
|
|
18
20
|
end
|
|
19
21
|
|
|
@@ -38,7 +40,7 @@ module Aikido::Zen
|
|
|
38
40
|
if key?(key)
|
|
39
41
|
@data[key].value
|
|
40
42
|
else
|
|
41
|
-
|
|
43
|
+
default_value
|
|
42
44
|
end
|
|
43
45
|
end
|
|
44
46
|
|
|
@@ -62,6 +64,10 @@ module Aikido::Zen
|
|
|
62
64
|
def to_h
|
|
63
65
|
to_a.to_h
|
|
64
66
|
end
|
|
67
|
+
|
|
68
|
+
private def default_value
|
|
69
|
+
@default_value || @initialize_block&.call
|
|
70
|
+
end
|
|
65
71
|
end
|
|
66
72
|
|
|
67
73
|
class CacheEntry
|
|
@@ -39,7 +39,7 @@ module Aikido::Zen
|
|
|
39
39
|
class TrackRequest < Event
|
|
40
40
|
register "track_request"
|
|
41
41
|
|
|
42
|
-
def self.from_json(
|
|
42
|
+
def self.from_json(_data)
|
|
43
43
|
new
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -52,6 +52,22 @@ module Aikido::Zen
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
class TrackRateLimitedRequest < Event
|
|
56
|
+
register "track_rate_limited_request"
|
|
57
|
+
|
|
58
|
+
def self.from_json(_data)
|
|
59
|
+
new
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def handle(collector)
|
|
63
|
+
collector.handle_track_rate_limited_request
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def inspect
|
|
67
|
+
"#<#{self.class.name}>"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
55
71
|
class TrackUserAgent < Event
|
|
56
72
|
register "track_user_agent"
|
|
57
73
|
|
|
@@ -79,6 +95,33 @@ module Aikido::Zen
|
|
|
79
95
|
end
|
|
80
96
|
end
|
|
81
97
|
|
|
98
|
+
class TrackIPList < Event
|
|
99
|
+
register "track_ip_list"
|
|
100
|
+
|
|
101
|
+
def self.from_json(data)
|
|
102
|
+
new(data[:ip_list_keys])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def initialize(ip_list_keys)
|
|
106
|
+
super()
|
|
107
|
+
@ip_list_keys = ip_list_keys
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def as_json
|
|
111
|
+
super.update({
|
|
112
|
+
ip_list_keys: @ip_list_keys
|
|
113
|
+
})
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def handle(collector)
|
|
117
|
+
collector.handle_track_ip_list(@ip_list_keys)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def inspect
|
|
121
|
+
"#<#{self.class.name} #{@ip_list_keys.inspect}>"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
82
125
|
class TrackAttackWave < Event
|
|
83
126
|
register "track_attack_wave"
|
|
84
127
|
|
|
@@ -8,7 +8,7 @@ module Aikido::Zen
|
|
|
8
8
|
# Tracks information about how the Aikido Agent is used in the app.
|
|
9
9
|
class Collector::Stats
|
|
10
10
|
# @!visibility private
|
|
11
|
-
attr_reader :started_at, :ended_at, :requests, :aborted_requests, :user_agents, :attack_waves, :blocked_attack_waves, :sinks
|
|
11
|
+
attr_reader :started_at, :ended_at, :requests, :aborted_requests, :rate_limited_requests, :user_agents, :ip_lists, :attack_waves, :blocked_attack_waves, :sinks
|
|
12
12
|
|
|
13
13
|
# @!visibility private
|
|
14
14
|
attr_writer :ended_at
|
|
@@ -20,7 +20,9 @@ module Aikido::Zen
|
|
|
20
20
|
@started_at = @ended_at = nil
|
|
21
21
|
@requests = 0
|
|
22
22
|
@aborted_requests = 0
|
|
23
|
+
@rate_limited_requests = 0
|
|
23
24
|
@user_agents = Hash.new { |h, k| h[k] = 0 }
|
|
25
|
+
@ip_lists = Hash.new { |h, k| h[k] = 0 }
|
|
24
26
|
@attack_waves = 0
|
|
25
27
|
@blocked_attack_waves = 0
|
|
26
28
|
@sinks = Hash.new { |h, k| h[k] = Collector::SinkStats.new(k, @config) }
|
|
@@ -67,12 +69,23 @@ module Aikido::Zen
|
|
|
67
69
|
@requests += 1
|
|
68
70
|
end
|
|
69
71
|
|
|
72
|
+
# @return [void]
|
|
73
|
+
def add_rate_limited_request
|
|
74
|
+
@rate_limited_requests += 1
|
|
75
|
+
end
|
|
76
|
+
|
|
70
77
|
# @param user_agent_keys [Array<String>] the user agent keys
|
|
71
78
|
# @return [void]
|
|
72
79
|
def add_user_agent(user_agent_keys)
|
|
73
80
|
user_agent_keys&.each { |user_agent_key| @user_agents[user_agent_key] += 1 }
|
|
74
81
|
end
|
|
75
82
|
|
|
83
|
+
# @param user_agent_keys [Array<String>] the user agent keys
|
|
84
|
+
# @return [void]
|
|
85
|
+
def add_ip_list(ip_list_keys)
|
|
86
|
+
ip_list_keys&.each { |ip_list_key| @ip_lists[ip_list_key] += 1 }
|
|
87
|
+
end
|
|
88
|
+
|
|
76
89
|
# @param being_blocked [Boolean] whether the Agent blocked the request
|
|
77
90
|
# @return [void]
|
|
78
91
|
def add_attack_wave(being_blocked:)
|
|
@@ -109,6 +122,7 @@ module Aikido::Zen
|
|
|
109
122
|
requests: {
|
|
110
123
|
total: @requests,
|
|
111
124
|
aborted: @aborted_requests,
|
|
125
|
+
rateLimited: @rate_limited_requests,
|
|
112
126
|
attacksDetected: {
|
|
113
127
|
total: total_attacks,
|
|
114
128
|
blocked: total_blocked
|
|
@@ -120,6 +134,9 @@ module Aikido::Zen
|
|
|
120
134
|
},
|
|
121
135
|
userAgents: {
|
|
122
136
|
breakdown: @user_agents
|
|
137
|
+
},
|
|
138
|
+
ipAddresses: {
|
|
139
|
+
breakdown: @ip_lists
|
|
123
140
|
}
|
|
124
141
|
}
|
|
125
142
|
end
|
data/lib/aikido/zen/collector.rb
CHANGED
|
@@ -88,12 +88,23 @@ module Aikido::Zen
|
|
|
88
88
|
synchronize(@stats) { |stats| stats.add_request }
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# Track stats about the rate_limited_requests
|
|
92
|
+
#
|
|
93
|
+
# @return [void]
|
|
94
|
+
def track_rate_limited_request
|
|
95
|
+
add_event(Events::TrackRateLimitedRequest.new)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def handle_track_rate_limited_request
|
|
99
|
+
synchronize(@stats) { |stats| stats.add_rate_limited_request }
|
|
100
|
+
end
|
|
101
|
+
|
|
91
102
|
# Track stats about monitored and blocked user agents
|
|
92
103
|
#
|
|
93
|
-
# @param [Array<String>, nil] the user agent keys
|
|
104
|
+
# @param user_agent_keys [Array<String>, nil] the user agent keys
|
|
94
105
|
# @return [void]
|
|
95
106
|
def track_user_agent(user_agent_keys)
|
|
96
|
-
return if user_agent_keys.nil?
|
|
107
|
+
return if user_agent_keys.nil? || user_agent_keys.empty?
|
|
97
108
|
|
|
98
109
|
add_event(Events::TrackUserAgent.new(user_agent_keys))
|
|
99
110
|
end
|
|
@@ -102,6 +113,20 @@ module Aikido::Zen
|
|
|
102
113
|
synchronize(@stats) { |stats| stats.add_user_agent(user_agent_keys) }
|
|
103
114
|
end
|
|
104
115
|
|
|
116
|
+
# Track stats about blocked and monitored IP lists
|
|
117
|
+
#
|
|
118
|
+
# @param ip_list_keys [Array<String>, nil]
|
|
119
|
+
# @return [void]
|
|
120
|
+
def track_ip_list(ip_list_keys)
|
|
121
|
+
return if ip_list_keys.nil? || ip_list_keys.empty?
|
|
122
|
+
|
|
123
|
+
add_event(Events::TrackIPList.new(ip_list_keys))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def handle_track_ip_list(ip_list_keys)
|
|
127
|
+
synchronize(@stats) { |stats| stats.add_ip_list(ip_list_keys) }
|
|
128
|
+
end
|
|
129
|
+
|
|
105
130
|
# Track stats about an attack detected by our scanners.
|
|
106
131
|
#
|
|
107
132
|
# @param attack [Aikido::Zen::Events::AttackWave]
|
data/lib/aikido/zen/config.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "uri"
|
|
4
4
|
require "json"
|
|
5
5
|
require "logger"
|
|
6
|
+
require "digest"
|
|
6
7
|
|
|
7
8
|
require_relative "context"
|
|
8
9
|
|
|
@@ -11,7 +12,7 @@ module Aikido::Zen
|
|
|
11
12
|
# @return [Class, Integer, nil] The Rack middleware class or index after which
|
|
12
13
|
# the Zen middleware should be inserted. When set to nil, the middleware is
|
|
13
14
|
# inserted before the first middleware in the then-current middleware stack.
|
|
14
|
-
# Defaults to ::ActionDispatch::
|
|
15
|
+
# Defaults to ::ActionDispatch::RemoteIp.
|
|
15
16
|
attr_accessor :insert_middleware_after
|
|
16
17
|
|
|
17
18
|
# @return [Boolean] whether Aikido should be turned completely off (no
|
|
@@ -62,8 +63,8 @@ module Aikido::Zen
|
|
|
62
63
|
attr_reader :logger
|
|
63
64
|
|
|
64
65
|
# @return [String] Path of the socket where the detached agent will listen.
|
|
65
|
-
# By default,
|
|
66
|
-
# `aikido-detached-agent.sock
|
|
66
|
+
# By default, the socket file is created in the current working directory.
|
|
67
|
+
# Defaults to `aikido-detached-agent.sock`.
|
|
67
68
|
attr_accessor :detached_agent_socket_path
|
|
68
69
|
|
|
69
70
|
# @return [Boolean] is the agent in debugging mode?
|
|
@@ -94,7 +95,7 @@ module Aikido::Zen
|
|
|
94
95
|
# the oldest seen users.
|
|
95
96
|
attr_accessor :max_users_tracked
|
|
96
97
|
|
|
97
|
-
# @return [Proc{(Aikido::Zen::Request, Symbol) => Array(Integer, Hash, #each)}]
|
|
98
|
+
# @return [Proc{(Aikido::Zen::Request, Symbol, reason: String=nil) => Array(Integer, Hash, #each)}]
|
|
98
99
|
# Rack handler used to respond to requests from IPs, users or others blocked in the Aikido
|
|
99
100
|
# dashboard.
|
|
100
101
|
attr_accessor :blocked_responder
|
|
@@ -183,8 +184,12 @@ module Aikido::Zen
|
|
|
183
184
|
# Defaults to 10,000 entries.
|
|
184
185
|
attr_accessor :attack_wave_max_cache_entries
|
|
185
186
|
|
|
187
|
+
# @return [Integer] the maximum number of samples in the LRU cache.
|
|
188
|
+
# Defaults to 15 entries.
|
|
189
|
+
attr_accessor :attack_wave_max_cache_samples
|
|
190
|
+
|
|
186
191
|
def initialize
|
|
187
|
-
self.insert_middleware_after = ::ActionDispatch::
|
|
192
|
+
self.insert_middleware_after = ::ActionDispatch::RemoteIp
|
|
188
193
|
self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
|
|
189
194
|
self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
|
|
190
195
|
self.api_timeouts = 10
|
|
@@ -221,6 +226,7 @@ module Aikido::Zen
|
|
|
221
226
|
self.attack_wave_min_time_between_requests = 60 * 1000 # 1 min (ms)
|
|
222
227
|
self.attack_wave_min_time_between_events = 20 * 60 * 1000 # 20 min (ms)
|
|
223
228
|
self.attack_wave_max_cache_entries = 10_000
|
|
229
|
+
self.attack_wave_max_cache_samples = 15
|
|
224
230
|
end
|
|
225
231
|
|
|
226
232
|
# Set the base URL for API requests.
|
|
@@ -263,12 +269,32 @@ module Aikido::Zen
|
|
|
263
269
|
@api_timeouts.update(value)
|
|
264
270
|
end
|
|
265
271
|
|
|
272
|
+
def api_token_hash
|
|
273
|
+
return unless api_token
|
|
274
|
+
|
|
275
|
+
@api_token_hash ||= Digest::SHA1.hexdigest(api_token)[0, 7]
|
|
276
|
+
end
|
|
277
|
+
|
|
266
278
|
def detached_agent_socket_uri
|
|
267
279
|
"drbunix:" + @detached_agent_socket_path
|
|
268
280
|
end
|
|
269
281
|
|
|
282
|
+
def expanded_detached_agent_socket_path
|
|
283
|
+
@exanded_detached_agent_path ||= expand_socket_path(detached_agent_socket_path)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def expanded_detached_agent_socket_uri
|
|
287
|
+
@exanded_detached_agent_uri ||= expand_socket_path(detached_agent_socket_uri)
|
|
288
|
+
end
|
|
289
|
+
|
|
270
290
|
private
|
|
271
291
|
|
|
292
|
+
def expand_socket_path(socket_path)
|
|
293
|
+
socket_path = socket_path.dup
|
|
294
|
+
socket_path.gsub!("%h", api_token_hash) if api_token_hash
|
|
295
|
+
socket_path
|
|
296
|
+
end
|
|
297
|
+
|
|
272
298
|
def read_boolean_from_env(value)
|
|
273
299
|
return value unless value.respond_to?(:to_str)
|
|
274
300
|
|
|
@@ -293,13 +319,21 @@ module Aikido::Zen
|
|
|
293
319
|
DEFAULT_JSON_DECODER = JSON.method(:parse)
|
|
294
320
|
|
|
295
321
|
# @!visibility private
|
|
296
|
-
DEFAULT_DETACHED_AGENT_SOCKET_PATH = "aikido-detached-agent.sock"
|
|
322
|
+
DEFAULT_DETACHED_AGENT_SOCKET_PATH = "aikido-detached-agent.%h.sock"
|
|
297
323
|
|
|
298
324
|
# @!visibility private
|
|
299
|
-
DEFAULT_BLOCKED_RESPONDER = ->(request, blocking_type) do
|
|
325
|
+
DEFAULT_BLOCKED_RESPONDER = ->(request, blocking_type, reason = nil) do
|
|
300
326
|
message = case blocking_type
|
|
301
327
|
when :ip
|
|
302
|
-
|
|
328
|
+
"Your IP address is not allowed to access this resource. (Your IP: #{request.ip})"
|
|
329
|
+
when :ip_allowed_list
|
|
330
|
+
"Your IP address is not allowed to access this resource. (Your IP: #{request.client_ip})"
|
|
331
|
+
when :ip_blocked_list
|
|
332
|
+
if reason.nil?
|
|
333
|
+
"Your IP is blocked."
|
|
334
|
+
else
|
|
335
|
+
"Your IP is blocked due to #{reason}."
|
|
336
|
+
end
|
|
303
337
|
when :user_agent
|
|
304
338
|
"You are not allowed to access this resource because you have been identified as a bot."
|
|
305
339
|
else
|
|
@@ -315,7 +349,7 @@ module Aikido::Zen
|
|
|
315
349
|
|
|
316
350
|
# @!visibility private
|
|
317
351
|
DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
|
|
318
|
-
request.actor ? "actor:#{request.actor.id}" : request.
|
|
352
|
+
request.actor ? "actor:#{request.actor.id}" : request.client_ip
|
|
319
353
|
}
|
|
320
354
|
end
|
|
321
355
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The current context is stored in an additional Fiber instance variable and
|
|
4
|
+
# is though the aikido_current_context accessor methods.
|
|
5
|
+
|
|
6
|
+
class Fiber
|
|
7
|
+
# @api private
|
|
8
|
+
attr_accessor :aikido_current_context
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# When a new Fiber is instantiated the current context of the Fiber that is
|
|
12
|
+
# creating the new Fiber is copied into the new Fiber.
|
|
13
|
+
|
|
14
|
+
class << Fiber
|
|
15
|
+
# @api private
|
|
16
|
+
alias_method :new__internal_for_aikido_zen, :new
|
|
17
|
+
|
|
18
|
+
def new(*args, **kwargs, &blk)
|
|
19
|
+
context = Fiber.current.aikido_current_context
|
|
20
|
+
|
|
21
|
+
new__internal_for_aikido_zen(*args, **kwargs) do |*args|
|
|
22
|
+
Fiber.current.aikido_current_context = context
|
|
23
|
+
|
|
24
|
+
blk.call(*args)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -34,7 +34,7 @@ module Aikido::Zen::DetachedAgent
|
|
|
34
34
|
|
|
35
35
|
@collector = collector
|
|
36
36
|
|
|
37
|
-
@front_object = DRbObject.new_with_uri(config.
|
|
37
|
+
@front_object = DRbObject.new_with_uri(config.expanded_detached_agent_socket_uri)
|
|
38
38
|
|
|
39
39
|
@has_forked = false
|
|
40
40
|
schedule_tasks
|
|
@@ -46,7 +46,7 @@ module Aikido::Zen::DetachedAgent
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def calculate_rate_limits(request)
|
|
49
|
-
@front_object.calculate_rate_limits(request.route.as_json, request.
|
|
49
|
+
@front_object.calculate_rate_limits(request.route.as_json, request.client_ip, request.actor.as_json)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Every time a fork occurs (a new child process is created), we need to start
|
|
@@ -17,7 +17,7 @@ module Aikido::Zen::DetachedAgent
|
|
|
17
17
|
@rate_limiter = rate_limiter
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
RequestKind = Struct.new(:route, :schema, :
|
|
20
|
+
RequestKind = Struct.new(:route, :schema, :client_ip, :actor)
|
|
21
21
|
|
|
22
22
|
def send_collector_events(events_data)
|
|
23
23
|
events_data.each do |event_data|
|
|
@@ -16,8 +16,8 @@ module Aikido::Zen::DetachedAgent
|
|
|
16
16
|
|
|
17
17
|
@config = config
|
|
18
18
|
|
|
19
|
-
@socket_path = config.
|
|
20
|
-
@socket_uri = config.
|
|
19
|
+
@socket_path = config.expanded_detached_agent_socket_path
|
|
20
|
+
@socket_uri = config.expanded_detached_agent_socket_uri
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def started?
|
data/lib/aikido/zen/errors.rb
CHANGED
data/lib/aikido/zen/event.rb
CHANGED
|
@@ -71,23 +71,6 @@ module Aikido::Zen
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
class AttackWave < Event
|
|
74
|
-
# @param [Aikido::Zen::Context] a context
|
|
75
|
-
# @return [Aikido::Zen::Events::AttackWave] an attack wave event
|
|
76
|
-
def self.from_context(context)
|
|
77
|
-
request = Aikido::Zen::AttackWave::Request.new(
|
|
78
|
-
ip_address: context.request.client_ip,
|
|
79
|
-
user_agent: context.request.user_agent,
|
|
80
|
-
source: context.request.framework
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
attack = Aikido::Zen::AttackWave::Attack.new(
|
|
84
|
-
metadata: {}, # not used yet
|
|
85
|
-
user: context.request.actor
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
new(request: request, attack: attack)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
74
|
# @return [Aikido::Zen::AttackWave::Request]
|
|
92
75
|
attr_reader :request
|
|
93
76
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Aikido::Zen
|
|
4
4
|
module Middleware
|
|
5
|
-
# Middleware that
|
|
5
|
+
# Middleware that only allows allowed IPs when allowed IPs are configured for
|
|
6
|
+
# any matching route in the Aikido dashboard.
|
|
6
7
|
class AllowedAddressChecker
|
|
7
8
|
def initialize(app, config: Aikido::Zen.config, settings: Aikido::Zen.runtime_settings)
|
|
8
9
|
@app = app
|
|
@@ -13,14 +14,21 @@ module Aikido::Zen
|
|
|
13
14
|
def call(env)
|
|
14
15
|
request = Aikido::Zen::Middleware.request_from(env)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if allowed_ips.empty? || allowed_ips.include?(request.ip)
|
|
17
|
+
if allowed?(request)
|
|
19
18
|
@app.call(env)
|
|
20
19
|
else
|
|
21
20
|
@config.blocked_responder.call(request, :ip)
|
|
22
21
|
end
|
|
23
22
|
end
|
|
23
|
+
|
|
24
|
+
private def allowed?(request)
|
|
25
|
+
return true if @settings.bypassed_ips.include?(request.client_ip)
|
|
26
|
+
|
|
27
|
+
matches = @settings.endpoints.matched_settings(request.route)
|
|
28
|
+
|
|
29
|
+
matches.all? { |settings| settings.allowed_ips.empty? } ||
|
|
30
|
+
matches.any? { |settings| settings.allowed_ips.include?(request.ip) }
|
|
31
|
+
end
|
|
24
32
|
end
|
|
25
33
|
end
|
|
26
34
|
end
|
|
@@ -19,10 +19,9 @@ module Aikido::Zen
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
private def protection_disabled?(request)
|
|
22
|
-
|
|
23
|
-
return true if @settings.allowed_ips.include?(request.ip)
|
|
22
|
+
return true if @settings.bypassed_ips.include?(request.client_ip)
|
|
24
23
|
|
|
25
|
-
!@settings.endpoints.
|
|
24
|
+
!@settings.endpoints.matched_settings(request.route).all?(&:protected?)
|
|
26
25
|
end
|
|
27
26
|
end
|
|
28
27
|
end
|