aikido-zen 1.1.1 → 1.2.1
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 +19 -6
- data/lib/aikido/zen/current_context.rb +27 -0
- data/lib/aikido/zen/detached_agent/agent.rb +1 -1
- data/lib/aikido/zen/detached_agent/front_object.rb +1 -1
- 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/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 +15 -3
- 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: c6c3172c2e8ea724f5b362592c7c9d326396c2ff05ba98a1573ed7ed587a1e55
|
|
4
|
+
data.tar.gz: 7c63642333e1f64c6b3011a12f52637438b98348e94754da47cb72f3c84f10ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32e2a25c9f2f55a21a3bf2d6e487b36ec3b8780dcaae3db9a82324669a99bf8c0514a6bbe9c810c20b877039e0827fa2446c053f610cf1317b3d5f064bc1e653
|
|
7
|
+
data.tar.gz: 41dda121db561eea1d99b0d4c76df3672e1d6e210e9d841e48ea467418120b3ce665151dc7dae9d889593300c40da1738bf3fa2266084fd7af603882146d21e3
|
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
|
@@ -12,7 +12,7 @@ module Aikido::Zen
|
|
|
12
12
|
# @return [Class, Integer, nil] The Rack middleware class or index after which
|
|
13
13
|
# the Zen middleware should be inserted. When set to nil, the middleware is
|
|
14
14
|
# inserted before the first middleware in the then-current middleware stack.
|
|
15
|
-
# Defaults to ::ActionDispatch::
|
|
15
|
+
# Defaults to ::ActionDispatch::RemoteIp.
|
|
16
16
|
attr_accessor :insert_middleware_after
|
|
17
17
|
|
|
18
18
|
# @return [Boolean] whether Aikido should be turned completely off (no
|
|
@@ -95,7 +95,7 @@ module Aikido::Zen
|
|
|
95
95
|
# the oldest seen users.
|
|
96
96
|
attr_accessor :max_users_tracked
|
|
97
97
|
|
|
98
|
-
# @return [Proc{(Aikido::Zen::Request, Symbol) => Array(Integer, Hash, #each)}]
|
|
98
|
+
# @return [Proc{(Aikido::Zen::Request, Symbol, reason: String=nil) => Array(Integer, Hash, #each)}]
|
|
99
99
|
# Rack handler used to respond to requests from IPs, users or others blocked in the Aikido
|
|
100
100
|
# dashboard.
|
|
101
101
|
attr_accessor :blocked_responder
|
|
@@ -184,8 +184,12 @@ module Aikido::Zen
|
|
|
184
184
|
# Defaults to 10,000 entries.
|
|
185
185
|
attr_accessor :attack_wave_max_cache_entries
|
|
186
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
|
+
|
|
187
191
|
def initialize
|
|
188
|
-
self.insert_middleware_after = ::ActionDispatch::
|
|
192
|
+
self.insert_middleware_after = ::ActionDispatch::RemoteIp
|
|
189
193
|
self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
|
|
190
194
|
self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
|
|
191
195
|
self.api_timeouts = 10
|
|
@@ -222,6 +226,7 @@ module Aikido::Zen
|
|
|
222
226
|
self.attack_wave_min_time_between_requests = 60 * 1000 # 1 min (ms)
|
|
223
227
|
self.attack_wave_min_time_between_events = 20 * 60 * 1000 # 20 min (ms)
|
|
224
228
|
self.attack_wave_max_cache_entries = 10_000
|
|
229
|
+
self.attack_wave_max_cache_samples = 15
|
|
225
230
|
end
|
|
226
231
|
|
|
227
232
|
# Set the base URL for API requests.
|
|
@@ -317,10 +322,18 @@ module Aikido::Zen
|
|
|
317
322
|
DEFAULT_DETACHED_AGENT_SOCKET_PATH = "aikido-detached-agent.%h.sock"
|
|
318
323
|
|
|
319
324
|
# @!visibility private
|
|
320
|
-
DEFAULT_BLOCKED_RESPONDER = ->(request, blocking_type) do
|
|
325
|
+
DEFAULT_BLOCKED_RESPONDER = ->(request, blocking_type, reason = nil) do
|
|
321
326
|
message = case blocking_type
|
|
322
327
|
when :ip
|
|
323
|
-
|
|
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
|
|
324
337
|
when :user_agent
|
|
325
338
|
"You are not allowed to access this resource because you have been identified as a bot."
|
|
326
339
|
else
|
|
@@ -336,7 +349,7 @@ module Aikido::Zen
|
|
|
336
349
|
|
|
337
350
|
# @!visibility private
|
|
338
351
|
DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
|
|
339
|
-
request.actor ? "actor:#{request.actor.id}" : request.
|
|
352
|
+
request.actor ? "actor:#{request.actor.id}" : request.client_ip
|
|
340
353
|
}
|
|
341
354
|
end
|
|
342
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
|
|
@@ -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|
|
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
|
|
@@ -25,8 +25,7 @@ module Aikido
|
|
|
25
25
|
request = context.request
|
|
26
26
|
return false if request.nil?
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
return false if @settings.allowed_ips.include?(request.ip)
|
|
28
|
+
return false if @settings.bypassed_ips.include?(request.client_ip)
|
|
30
29
|
|
|
31
30
|
@zen.attack_wave_detector.attack_wave?(context)
|
|
32
31
|
end
|
|
@@ -35,7 +34,26 @@ module Aikido
|
|
|
35
34
|
# Visible for testing.
|
|
36
35
|
def protect(context)
|
|
37
36
|
if attack_wave?(context)
|
|
38
|
-
|
|
37
|
+
client_ip = context.request.client_ip
|
|
38
|
+
|
|
39
|
+
request = Aikido::Zen::AttackWave::Request.new(
|
|
40
|
+
ip_address: client_ip,
|
|
41
|
+
user_agent: context.request.user_agent,
|
|
42
|
+
source: context.request.framework
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
samples = @zen.attack_wave_detector.samples[client_ip].to_a
|
|
46
|
+
|
|
47
|
+
attack = Aikido::Zen::AttackWave::Attack.new(
|
|
48
|
+
samples: samples,
|
|
49
|
+
user: context.request.actor
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
attack_wave = Aikido::Zen::Events::AttackWave.new(
|
|
53
|
+
request: request,
|
|
54
|
+
attack: attack
|
|
55
|
+
)
|
|
56
|
+
|
|
39
57
|
@zen.track_attack_wave(attack_wave)
|
|
40
58
|
@zen.agent.report(attack_wave)
|
|
41
59
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen
|
|
4
|
+
module Middleware
|
|
5
|
+
class IPListChecker
|
|
6
|
+
def initialize(app, zen: Aikido::Zen, config: zen.config, settings: zen.runtime_settings)
|
|
7
|
+
@app = app
|
|
8
|
+
@zen = zen
|
|
9
|
+
@config = config
|
|
10
|
+
@settings = settings
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
request = Aikido::Zen::Middleware.request_from(env)
|
|
15
|
+
|
|
16
|
+
client_ip = request.client_ip
|
|
17
|
+
|
|
18
|
+
return @app.call(env) if bypassed_ip?(client_ip)
|
|
19
|
+
|
|
20
|
+
if !@settings.allowed_ip?(client_ip)
|
|
21
|
+
return @config.blocked_responder.call(request, :ip_allowed_list)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
monitored_ip_list_keys = @settings.monitored_ip_list_keys(client_ip)
|
|
25
|
+
@zen.track_ip_list(monitored_ip_list_keys)
|
|
26
|
+
|
|
27
|
+
blocked_ip_lists = @settings.blocked_ip_lists.filter { |ip_list| ip_list.include?(client_ip) }
|
|
28
|
+
|
|
29
|
+
if !blocked_ip_lists.empty?
|
|
30
|
+
@zen.track_ip_list(blocked_ip_lists.map(&:key))
|
|
31
|
+
|
|
32
|
+
return @config.blocked_responder.call(
|
|
33
|
+
request,
|
|
34
|
+
:ip_blocked_list,
|
|
35
|
+
blocked_ip_lists.first.description
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@app.call(env)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def bypassed_ip?(client_ip)
|
|
43
|
+
@settings.bypassed_ips.include?(client_ip)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|