aikido-zen 1.7.2.beta.1 → 1.7.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/request-bypassing.md +38 -0
- data/lib/aikido/zen/attack_wave/helpers.rb +10 -0
- data/lib/aikido/zen/attack_wave.rb +52 -16
- data/lib/aikido/zen/cache.rb +3 -3
- data/lib/aikido/zen/collector/routes.rb +2 -2
- data/lib/aikido/zen/collector.rb +0 -5
- data/lib/aikido/zen/context.rb +5 -0
- data/lib/aikido/zen/idor/protector.rb +1 -1
- data/lib/aikido/zen/middleware/allowed_address_checker.rb +3 -2
- data/lib/aikido/zen/middleware/attack_protector.rb +1 -1
- data/lib/aikido/zen/middleware/attack_wave_protector.rb +29 -27
- data/lib/aikido/zen/middleware/ip_list_checker.rb +1 -5
- data/lib/aikido/zen/middleware/rack_throttler.rb +1 -1
- data/lib/aikido/zen/middleware/request_tracker.rb +8 -8
- data/lib/aikido/zen/middleware/user_agent_checker.rb +1 -5
- data/lib/aikido/zen/sinks/action_controller.rb +1 -1
- data/lib/aikido/zen/sinks/async_http.rb +1 -3
- data/lib/aikido/zen/sinks/curb.rb +1 -3
- data/lib/aikido/zen/sinks/em_http.rb +1 -3
- data/lib/aikido/zen/sinks/excon.rb +1 -3
- data/lib/aikido/zen/sinks/http.rb +1 -3
- data/lib/aikido/zen/sinks/httpclient.rb +1 -3
- data/lib/aikido/zen/sinks/httpx.rb +1 -3
- data/lib/aikido/zen/sinks/net_http.rb +1 -3
- data/lib/aikido/zen/sinks/patron.rb +1 -3
- data/lib/aikido/zen/sinks/typhoeus.rb +1 -3
- data/lib/aikido/zen/version.rb +1 -1
- data/lib/aikido/zen/worker_process/agent/client.rb +19 -0
- data/lib/aikido/zen/worker_process/agent/server.rb +14 -3
- data/lib/aikido/zen.rb +60 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41f1982e134a806c513cc166247059c3f290f6bbc88ff32dcf4bdc65da9f377d
|
|
4
|
+
data.tar.gz: d97dac2f830555239836a143b815ca3c85c4993776183ce73bc5dc392fed9618
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 526140c45a47c29f4f36e1508bc3305bafe535e92479c877deeb92a1f74c618fa19d7c46a7d67183310fbc414f05912b93c88f2cdb109730d04c0b4b6a54b301
|
|
7
|
+
data.tar.gz: 6c43bc7461a52ded8331b6ba2188afd9d273e1b012f5912b2d9786c439e2890380003779925bd3f37fdd4422f4b34aefe13af6d29d0a6a7f3b4e351c5019dca1
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Bypass Zen for a specific request
|
|
2
|
+
|
|
3
|
+
To disable Zen for a specific request, call `Aikido::Zen.request_bypassed!` from a Rack middleware. This bypasses Zen's security checks and stats collection for that request.
|
|
4
|
+
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> Zen already has a built-in feature for bypassing specific IP addresses. This feature offers more flexibility, letting you bypass Zen for a request based on any criteria you choose.
|
|
7
|
+
|
|
8
|
+
`Aikido::Zen.request_bypassed!` needs the request's context to already be set up, so your middleware must run after Zen's `ContextSetter` middleware. The following example shows how to bypass a specific request in a Rails application:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
# config/application.rb
|
|
12
|
+
class RequestBypasser
|
|
13
|
+
def initialize(app)
|
|
14
|
+
@app = app
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(env)
|
|
18
|
+
if your_custom_logic?(env)
|
|
19
|
+
Aikido::Zen.request_bypassed! # Bypasses Zen for this request
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@app.call(env)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module YourApp
|
|
27
|
+
class Application < Rails::Application
|
|
28
|
+
# ...
|
|
29
|
+
|
|
30
|
+
initializer "your_app.insert_request_bypasser", after: "aikido.add_middleware" do |app|
|
|
31
|
+
app.middleware.insert_after Aikido::Zen::Middleware::ContextSetter, RequestBypasser
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> [!WARNING]
|
|
38
|
+
> Use this feature with caution, as it can potentially expose your application to security risks if not used properly.
|
|
@@ -13,6 +13,16 @@ module Aikido::Zen
|
|
|
13
13
|
false
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# Builds the sample for a request that has been classified as suspicious.
|
|
17
|
+
#
|
|
18
|
+
# @param context [Aikido::Zen::Context]
|
|
19
|
+
# @return [Aikido::Zen::AttackWave::Sample]
|
|
20
|
+
def self.sample_for(context)
|
|
21
|
+
request = context.request
|
|
22
|
+
|
|
23
|
+
Sample.new(verb: request.request_method, path: original_fullpath(request))
|
|
24
|
+
end
|
|
25
|
+
|
|
16
26
|
def self.suspicious_request?(context, status_code)
|
|
17
27
|
request = context.request
|
|
18
28
|
|
|
@@ -5,6 +5,11 @@ require_relative "attack_wave/helpers"
|
|
|
5
5
|
|
|
6
6
|
module Aikido::Zen
|
|
7
7
|
module AttackWave
|
|
8
|
+
# Tracks per-client-IP attack wave state.
|
|
9
|
+
#
|
|
10
|
+
# In multiprocess deployments, the main process has a single instance
|
|
11
|
+
# that may be accessed concurrently from each forked worker process's
|
|
12
|
+
# Aikido::Zen::RPC::Server connection thread.
|
|
8
13
|
class Detector
|
|
9
14
|
# @return [Aikido::Zen::CappedSet]
|
|
10
15
|
attr_reader :samples
|
|
@@ -12,6 +17,8 @@ module Aikido::Zen
|
|
|
12
17
|
def initialize(config: Aikido::Zen.config, clock: nil)
|
|
13
18
|
@config = config
|
|
14
19
|
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
|
|
15
22
|
@event_times = Cache.new(@config.attack_wave_max_cache_entries, ttl: @config.attack_wave_min_time_between_events, clock: clock)
|
|
16
23
|
|
|
17
24
|
@request_counts = Cache.new(@config.attack_wave_max_cache_entries, 0, ttl: @config.attack_wave_min_time_between_requests, clock: clock)
|
|
@@ -21,29 +28,54 @@ module Aikido::Zen
|
|
|
21
28
|
end
|
|
22
29
|
end
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
# @api private
|
|
32
|
+
# @note Visible for testing.
|
|
33
|
+
#
|
|
34
|
+
# Whether the client IP is within the cooldown period after triggering
|
|
35
|
+
# an attack wave.
|
|
36
|
+
#
|
|
37
|
+
# @param client_ip [String]
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def flagged?(client_ip)
|
|
40
|
+
!!@event_times[client_ip]
|
|
41
|
+
end
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
# @api private
|
|
44
|
+
# @note Visible for testing.
|
|
45
|
+
#
|
|
46
|
+
# Flags the client IP as having triggered an attack wave for the
|
|
47
|
+
# cooldown period.
|
|
48
|
+
#
|
|
49
|
+
# @param client_ip [String]
|
|
50
|
+
# @return [void]
|
|
51
|
+
def flag!(client_ip)
|
|
52
|
+
@event_times[client_ip] = Time.now.utc
|
|
53
|
+
end
|
|
30
54
|
|
|
31
|
-
|
|
55
|
+
# Records a suspicious sample and, if it crosses the threshold for
|
|
56
|
+
# triggering an attack wave, flags the client IP as having just
|
|
57
|
+
# triggered an attack wave.
|
|
58
|
+
#
|
|
59
|
+
# This method is synchronized to prevent concurrent calls for the
|
|
60
|
+
# same client IP from crossing the threshold in the same instant.
|
|
61
|
+
#
|
|
62
|
+
# @param client_ip [String]
|
|
63
|
+
# @param sample [Aikido::Zen::AttackWave::Sample]
|
|
64
|
+
# @return [Array<Aikido::Zen::AttackWave::Sample>, nil]
|
|
65
|
+
def record(client_ip, sample)
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
return nil if flagged?(client_ip)
|
|
32
68
|
|
|
33
|
-
|
|
69
|
+
request_count = @request_counts[client_ip] += 1
|
|
34
70
|
|
|
35
|
-
|
|
36
|
-
@samples[client_ip] <<= Sample.new(
|
|
37
|
-
verb: request.request_method,
|
|
38
|
-
path: AttackWave::Helpers.original_fullpath(request)
|
|
39
|
-
)
|
|
40
|
-
end
|
|
71
|
+
@samples[client_ip] <<= sample
|
|
41
72
|
|
|
42
|
-
|
|
73
|
+
return nil if request_count < @config.attack_wave_threshold
|
|
43
74
|
|
|
44
|
-
|
|
75
|
+
flag!(client_ip)
|
|
45
76
|
|
|
46
|
-
|
|
77
|
+
@samples[client_ip].to_a
|
|
78
|
+
end
|
|
47
79
|
end
|
|
48
80
|
end
|
|
49
81
|
|
|
@@ -117,6 +149,10 @@ module Aikido::Zen
|
|
|
117
149
|
end
|
|
118
150
|
|
|
119
151
|
class Sample
|
|
152
|
+
def self.from_json(data)
|
|
153
|
+
new(verb: data["method"], path: data["url"])
|
|
154
|
+
end
|
|
155
|
+
|
|
120
156
|
# @return [String]
|
|
121
157
|
attr_reader :verb
|
|
122
158
|
|
data/lib/aikido/zen/cache.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Aikido::Zen
|
|
|
5
5
|
extend Forwardable
|
|
6
6
|
|
|
7
7
|
# @api private
|
|
8
|
-
# Visible for testing.
|
|
8
|
+
# @note Visible for testing.
|
|
9
9
|
def_delegators :@data,
|
|
10
10
|
:size, :empty?
|
|
11
11
|
|
|
@@ -54,13 +54,13 @@ module Aikido::Zen
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
# @api private
|
|
57
|
-
# Visible for testing.
|
|
57
|
+
# @note Visible for testing.
|
|
58
58
|
def to_a
|
|
59
59
|
@data.map { |key, entry| [key, entry.value] }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# @api private
|
|
63
|
-
# Visible for testing.
|
|
63
|
+
# @note Visible for testing.
|
|
64
64
|
def to_h
|
|
65
65
|
to_a.to_h
|
|
66
66
|
end
|
|
@@ -8,7 +8,7 @@ module Aikido::Zen
|
|
|
8
8
|
# Keeps track of the visited routes.
|
|
9
9
|
class Collector::Routes
|
|
10
10
|
# @api private
|
|
11
|
-
# Visible for testing.
|
|
11
|
+
# @note Visible for testing.
|
|
12
12
|
attr_reader :visits
|
|
13
13
|
|
|
14
14
|
def initialize(config = Aikido::Zen.config)
|
|
@@ -35,7 +35,7 @@ module Aikido::Zen
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# @api private
|
|
38
|
-
# Visible for testing.
|
|
38
|
+
# @note Visible for testing.
|
|
39
39
|
def [](route)
|
|
40
40
|
@visits[route]
|
|
41
41
|
end
|
data/lib/aikido/zen/collector.rb
CHANGED
|
@@ -210,7 +210,6 @@ module Aikido::Zen
|
|
|
210
210
|
end
|
|
211
211
|
|
|
212
212
|
# @api private
|
|
213
|
-
#
|
|
214
213
|
# @note Visible for testing.
|
|
215
214
|
def stats
|
|
216
215
|
handle
|
|
@@ -218,7 +217,6 @@ module Aikido::Zen
|
|
|
218
217
|
end
|
|
219
218
|
|
|
220
219
|
# @api private
|
|
221
|
-
#
|
|
222
220
|
# @note Visible for testing.
|
|
223
221
|
def users
|
|
224
222
|
handle
|
|
@@ -226,7 +224,6 @@ module Aikido::Zen
|
|
|
226
224
|
end
|
|
227
225
|
|
|
228
226
|
# @api private
|
|
229
|
-
#
|
|
230
227
|
# @note Visible for testing.
|
|
231
228
|
def hosts
|
|
232
229
|
handle
|
|
@@ -234,7 +231,6 @@ module Aikido::Zen
|
|
|
234
231
|
end
|
|
235
232
|
|
|
236
233
|
# @api private
|
|
237
|
-
#
|
|
238
234
|
# @note Visible for testing.
|
|
239
235
|
def routes
|
|
240
236
|
handle
|
|
@@ -242,7 +238,6 @@ module Aikido::Zen
|
|
|
242
238
|
end
|
|
243
239
|
|
|
244
240
|
# @api private
|
|
245
|
-
#
|
|
246
241
|
# @note Visible for testing.
|
|
247
242
|
def middleware_installed?
|
|
248
243
|
@middleware_installed.true?
|
data/lib/aikido/zen/context.rb
CHANGED
|
@@ -24,6 +24,10 @@ module Aikido::Zen
|
|
|
24
24
|
attr_accessor :scanning
|
|
25
25
|
alias_method :scanning?, :scanning
|
|
26
26
|
|
|
27
|
+
# @return [Boolean, nil] whether the request is bypassed.
|
|
28
|
+
attr_accessor :request_bypassed
|
|
29
|
+
alias_method :request_bypassed?, :request_bypassed
|
|
30
|
+
|
|
27
31
|
# @return [Boolean] whether attack protection for the currently requested
|
|
28
32
|
# endpoint was disabled on the Aikido dashboard, or if the source IP for
|
|
29
33
|
# this request is in the "Bypass List".
|
|
@@ -48,6 +52,7 @@ module Aikido::Zen
|
|
|
48
52
|
|
|
49
53
|
@metadata = {}
|
|
50
54
|
@scanning = false
|
|
55
|
+
@request_bypassed = nil
|
|
51
56
|
@protection_disabled = false
|
|
52
57
|
@idor_protection_enabled = false
|
|
53
58
|
end
|
|
@@ -5,8 +5,9 @@ module Aikido::Zen
|
|
|
5
5
|
# Middleware that only allows allowed IPs when allowed IPs are configured for
|
|
6
6
|
# any matching route in the Aikido dashboard.
|
|
7
7
|
class AllowedAddressChecker
|
|
8
|
-
def initialize(app,
|
|
8
|
+
def initialize(app, zen: Aikido::Zen, config: zen.config, settings: zen.runtime_settings)
|
|
9
9
|
@app = app
|
|
10
|
+
@zen = zen
|
|
10
11
|
@config = config
|
|
11
12
|
@settings = settings
|
|
12
13
|
end
|
|
@@ -22,7 +23,7 @@ module Aikido::Zen
|
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
private def allowed?(request)
|
|
25
|
-
return true if @
|
|
26
|
+
return true if @zen.request_bypassed?
|
|
26
27
|
|
|
27
28
|
matches = @settings.endpoints.matched_settings(request.route)
|
|
28
29
|
|
|
@@ -19,7 +19,7 @@ module Aikido::Zen
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
private def protection_disabled?(request)
|
|
22
|
-
return true if @
|
|
22
|
+
return true if @zen.request_bypassed?
|
|
23
23
|
|
|
24
24
|
!@settings.endpoints.matched_settings(request.route).all?(&:protected?)
|
|
25
25
|
end
|
|
@@ -4,10 +4,9 @@ module Aikido
|
|
|
4
4
|
module Zen
|
|
5
5
|
module Middleware
|
|
6
6
|
class AttackWaveProtector
|
|
7
|
-
def initialize(app, zen: Aikido::Zen
|
|
7
|
+
def initialize(app, zen: Aikido::Zen)
|
|
8
8
|
@app = app
|
|
9
9
|
@zen = zen
|
|
10
|
-
@settings = settings
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
def call(env)
|
|
@@ -21,43 +20,46 @@ module Aikido
|
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
# @api private
|
|
24
|
-
# Visible for testing.
|
|
25
|
-
|
|
23
|
+
# @note Visible for testing.
|
|
24
|
+
#
|
|
25
|
+
# @param context [Aikido::Zen::Context]
|
|
26
|
+
# @param status_code [Integer, nil]
|
|
27
|
+
# @return [Array<Aikido::Zen::AttackWave::Sample>, nil]
|
|
28
|
+
def detect_attack_wave(context, status_code = nil)
|
|
26
29
|
request = context.request
|
|
27
|
-
return
|
|
30
|
+
return nil if request.nil?
|
|
28
31
|
|
|
29
|
-
return
|
|
32
|
+
return nil if @zen.request_bypassed?
|
|
30
33
|
|
|
31
|
-
@zen.
|
|
34
|
+
@zen.detect_attack_wave(context, status_code)
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
# @api private
|
|
35
|
-
# Visible for testing.
|
|
38
|
+
# @note Visible for testing.
|
|
36
39
|
def protect(context, status_code = nil)
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
samples = detect_attack_wave(context, status_code)
|
|
41
|
+
return unless samples
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
ip_address: client_ip,
|
|
42
|
-
user_agent: context.request.user_agent,
|
|
43
|
-
source: context.request.framework
|
|
44
|
-
)
|
|
43
|
+
client_ip = context.request.client_ip
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
request = Aikido::Zen::AttackWave::Request.new(
|
|
46
|
+
ip_address: client_ip,
|
|
47
|
+
user_agent: context.request.user_agent,
|
|
48
|
+
source: context.request.framework
|
|
49
|
+
)
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
attack = Aikido::Zen::AttackWave::Attack.new(
|
|
52
|
+
samples: samples,
|
|
53
|
+
user: context.request.actor
|
|
54
|
+
)
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
attack_wave = Aikido::Zen::Events::AttackWave.new(
|
|
57
|
+
request: request,
|
|
58
|
+
attack: attack
|
|
59
|
+
)
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
end
|
|
61
|
+
@zen.track_attack_wave(attack_wave)
|
|
62
|
+
@zen.agent.report(attack_wave)
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
end
|
|
@@ -15,7 +15,7 @@ module Aikido::Zen
|
|
|
15
15
|
|
|
16
16
|
client_ip = request.client_ip
|
|
17
17
|
|
|
18
|
-
return @app.call(env) if
|
|
18
|
+
return @app.call(env) if @zen.request_bypassed?
|
|
19
19
|
|
|
20
20
|
if !@settings.allowed_ip?(client_ip)
|
|
21
21
|
return @config.blocked_responder.call(request, :ip_allowed_list)
|
|
@@ -38,10 +38,6 @@ module Aikido::Zen
|
|
|
38
38
|
|
|
39
39
|
@app.call(env)
|
|
40
40
|
end
|
|
41
|
-
|
|
42
|
-
def bypassed_ip?(client_ip)
|
|
43
|
-
@settings.bypassed_ips.include?(client_ip)
|
|
44
|
-
end
|
|
45
41
|
end
|
|
46
42
|
end
|
|
47
43
|
end
|
|
@@ -34,7 +34,7 @@ module Aikido::Zen
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
36
|
def should_throttle?(request)
|
|
37
|
-
return false if @
|
|
37
|
+
return false if @zen.request_bypassed?
|
|
38
38
|
|
|
39
39
|
return false if request.actor && @settings.user_excluded_from_rate_limiting?(request.actor.id)
|
|
40
40
|
|
|
@@ -5,8 +5,9 @@ module Aikido::Zen
|
|
|
5
5
|
# Rack middleware used to track request
|
|
6
6
|
# It implements the logic under that which is considered worthy of being tracked.
|
|
7
7
|
class RequestTracker
|
|
8
|
-
def initialize(app,
|
|
8
|
+
def initialize(app, zen: Aikido::Zen, settings: zen.runtime_settings)
|
|
9
9
|
@app = app
|
|
10
|
+
@zen = zen
|
|
10
11
|
@settings = settings
|
|
11
12
|
end
|
|
12
13
|
|
|
@@ -17,13 +18,12 @@ module Aikido::Zen
|
|
|
17
18
|
if request.route && track?(
|
|
18
19
|
status_code: response[0],
|
|
19
20
|
route: request.route.path,
|
|
20
|
-
http_method: request.request_method
|
|
21
|
-
ip: request.client_ip
|
|
21
|
+
http_method: request.request_method
|
|
22
22
|
)
|
|
23
|
-
|
|
23
|
+
@zen.track_request(request)
|
|
24
24
|
|
|
25
|
-
if
|
|
26
|
-
|
|
25
|
+
if @zen.config.collect_api_schema?
|
|
26
|
+
@zen.track_discovered_route(request)
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -128,8 +128,8 @@ module Aikido::Zen
|
|
|
128
128
|
# @param status_code [Integer]
|
|
129
129
|
# @param route [String]
|
|
130
130
|
# @param http_method [String]
|
|
131
|
-
def track?(status_code:, route:, http_method
|
|
132
|
-
return false if @
|
|
131
|
+
def track?(status_code:, route:, http_method:)
|
|
132
|
+
return false if @zen.request_bypassed?
|
|
133
133
|
|
|
134
134
|
# In the UI we want to show only successful (2xx) or redirect (3xx) responses
|
|
135
135
|
# anything else is discarded.
|
|
@@ -13,7 +13,7 @@ module Aikido::Zen
|
|
|
13
13
|
def call(env)
|
|
14
14
|
request = Aikido::Zen::Middleware.request_from(env)
|
|
15
15
|
|
|
16
|
-
return @app.call(env) if
|
|
16
|
+
return @app.call(env) if @zen.request_bypassed?
|
|
17
17
|
|
|
18
18
|
user_agent = request.user_agent
|
|
19
19
|
|
|
@@ -31,10 +31,6 @@ module Aikido::Zen
|
|
|
31
31
|
|
|
32
32
|
@app.call(env)
|
|
33
33
|
end
|
|
34
|
-
|
|
35
|
-
def bypassed?(request)
|
|
36
|
-
@settings.bypassed_ips.include?(request.client_ip)
|
|
37
|
-
end
|
|
38
34
|
end
|
|
39
35
|
end
|
|
40
36
|
end
|
|
@@ -30,7 +30,7 @@ module Aikido::Zen
|
|
|
30
30
|
|
|
31
31
|
request = context.request
|
|
32
32
|
|
|
33
|
-
return false if @
|
|
33
|
+
return false if @zen.request_bypassed?
|
|
34
34
|
|
|
35
35
|
if should_block_user?(request)
|
|
36
36
|
status, headers, body = @config.blocked_responder.call(request, :user)
|
|
@@ -52,9 +52,7 @@ module Aikido::Zen
|
|
|
52
52
|
|
|
53
53
|
settings = Aikido::Zen.runtime_settings
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
unless settings.bypassed_ip?(client_ip)
|
|
55
|
+
unless Aikido::Zen.request_bypassed?
|
|
58
56
|
Aikido::Zen.track_outbound(connection)
|
|
59
57
|
|
|
60
58
|
if settings.block_outbound?(connection)
|
|
@@ -64,9 +64,7 @@ module Aikido::Zen
|
|
|
64
64
|
|
|
65
65
|
settings = Aikido::Zen.runtime_settings
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
unless settings.bypassed_ip?(client_ip)
|
|
67
|
+
unless Aikido::Zen.request_bypassed?
|
|
70
68
|
Aikido::Zen.track_outbound(connection)
|
|
71
69
|
|
|
72
70
|
if settings.block_outbound?(connection)
|
|
@@ -50,9 +50,7 @@ module Aikido::Zen
|
|
|
50
50
|
|
|
51
51
|
settings = Aikido::Zen.runtime_settings
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
unless settings.bypassed_ip?(client_ip)
|
|
53
|
+
unless Aikido::Zen.request_bypassed?
|
|
56
54
|
Aikido::Zen.track_outbound(connection)
|
|
57
55
|
|
|
58
56
|
if settings.block_outbound?(connection)
|
|
@@ -56,9 +56,7 @@ module Aikido::Zen
|
|
|
56
56
|
|
|
57
57
|
settings = Aikido::Zen.runtime_settings
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
unless settings.bypassed_ip?(client_ip)
|
|
59
|
+
unless Aikido::Zen.request_bypassed?
|
|
62
60
|
Aikido::Zen.track_outbound(connection)
|
|
63
61
|
|
|
64
62
|
if settings.block_outbound?(connection)
|
|
@@ -70,9 +70,7 @@ module Aikido::Zen
|
|
|
70
70
|
|
|
71
71
|
settings = Aikido::Zen.runtime_settings
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
unless settings.bypassed_ip?(client_ip)
|
|
73
|
+
unless Aikido::Zen.request_bypassed?
|
|
76
74
|
Aikido::Zen.track_outbound(connection)
|
|
77
75
|
|
|
78
76
|
if settings.block_outbound?(connection)
|
|
@@ -50,9 +50,7 @@ module Aikido::Zen
|
|
|
50
50
|
|
|
51
51
|
settings = Aikido::Zen.runtime_settings
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
unless settings.bypassed_ip?(client_ip)
|
|
53
|
+
unless Aikido::Zen.request_bypassed?
|
|
56
54
|
Aikido::Zen.track_outbound(connection)
|
|
57
55
|
|
|
58
56
|
if settings.block_outbound?(connection)
|
|
@@ -55,9 +55,7 @@ module Aikido::Zen
|
|
|
55
55
|
|
|
56
56
|
settings = Aikido::Zen.runtime_settings
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
unless settings.bypassed_ip?(client_ip)
|
|
58
|
+
unless Aikido::Zen.request_bypassed?
|
|
61
59
|
Aikido::Zen.track_outbound(connection)
|
|
62
60
|
|
|
63
61
|
if settings.block_outbound?(connection)
|
|
@@ -86,9 +86,7 @@ module Aikido::Zen
|
|
|
86
86
|
|
|
87
87
|
settings = Aikido::Zen.runtime_settings
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
unless settings.bypassed_ip?(client_ip)
|
|
89
|
+
unless Aikido::Zen.request_bypassed?
|
|
92
90
|
Aikido::Zen.track_outbound(connection)
|
|
93
91
|
|
|
94
92
|
if settings.block_outbound?(connection)
|
|
@@ -59,9 +59,7 @@ module Aikido::Zen
|
|
|
59
59
|
|
|
60
60
|
settings = Aikido::Zen.runtime_settings
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
unless settings.bypassed_ip?(client_ip)
|
|
62
|
+
unless Aikido::Zen.request_bypassed?
|
|
65
63
|
Aikido::Zen.track_outbound(connection)
|
|
66
64
|
|
|
67
65
|
if settings.block_outbound?(connection)
|
|
@@ -26,9 +26,7 @@ module Aikido::Zen
|
|
|
26
26
|
|
|
27
27
|
settings = Aikido::Zen.runtime_settings
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
unless settings.bypassed_ip?(client_ip)
|
|
29
|
+
unless Aikido::Zen.request_bypassed?
|
|
32
30
|
Aikido::Zen.track_outbound(connection)
|
|
33
31
|
|
|
34
32
|
if settings.block_outbound?(connection)
|
data/lib/aikido/zen/version.rb
CHANGED
|
@@ -8,6 +8,11 @@ module Aikido::Zen::WorkerProcess
|
|
|
8
8
|
# The keepalive interval must be less than the RPC server read timeout.
|
|
9
9
|
KEEPALIVE_INTERVAL = 4
|
|
10
10
|
|
|
11
|
+
# This read timeout should be kept short because attackers can trigger
|
|
12
|
+
# attack wave detection checks at will, to limit how much latency an
|
|
13
|
+
# attacker can introduce per request.
|
|
14
|
+
ATTACK_WAVE_TIMEOUT = 1.0
|
|
15
|
+
|
|
11
16
|
def initialize(
|
|
12
17
|
host,
|
|
13
18
|
port,
|
|
@@ -68,6 +73,20 @@ module Aikido::Zen::WorkerProcess
|
|
|
68
73
|
raise
|
|
69
74
|
end
|
|
70
75
|
|
|
76
|
+
# @param client_ip [String]
|
|
77
|
+
# @param sample [Aikido::Zen::AttackWave::Sample]
|
|
78
|
+
# @return [Array<Aikido::Zen::AttackWave::Sample>, nil]
|
|
79
|
+
def record_attack_wave(client_ip, sample)
|
|
80
|
+
result = @rpc_client.invoke(
|
|
81
|
+
"record_attack_wave", ATTACK_WAVE_TIMEOUT,
|
|
82
|
+
client_ip, sample.verb, sample.path
|
|
83
|
+
)
|
|
84
|
+
result&.map { |data| Aikido::Zen::AttackWave::Sample.from_json(data) }
|
|
85
|
+
rescue => err
|
|
86
|
+
@config.logger.error("Forked worker process #{Process.pid}: failed to record attack wave sample with parent: #{err.message}")
|
|
87
|
+
raise
|
|
88
|
+
end
|
|
89
|
+
|
|
71
90
|
private
|
|
72
91
|
|
|
73
92
|
def updated_settings
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
module Aikido::Zen::WorkerProcess
|
|
4
4
|
module Agent
|
|
5
5
|
class Server
|
|
6
|
-
def initialize(config: Aikido::Zen.config)
|
|
6
|
+
def initialize(config: Aikido::Zen.config, detector: Aikido::Zen.attack_wave_detector)
|
|
7
7
|
@config = config
|
|
8
|
+
@detector = detector
|
|
8
9
|
|
|
9
10
|
@rpc_server = Aikido::Zen::RPC::Server.new(Aikido::Zen.secret)
|
|
10
11
|
|
|
@@ -25,6 +26,10 @@ module Aikido::Zen::WorkerProcess
|
|
|
25
26
|
result = calculate_rate_limits(route_data, ip, actor_data)
|
|
26
27
|
respond.call(result&.as_json)
|
|
27
28
|
end
|
|
29
|
+
|
|
30
|
+
@rpc_server.handle("record_attack_wave") do |respond, client_ip, verb, path|
|
|
31
|
+
respond.call(record_attack_wave(client_ip, verb, path))
|
|
32
|
+
end
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
def host
|
|
@@ -60,8 +65,7 @@ module Aikido::Zen::WorkerProcess
|
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
# @api private
|
|
63
|
-
#
|
|
64
|
-
# Visible for testing.
|
|
68
|
+
# @note Visible for testing.
|
|
65
69
|
RequestKind = Struct.new(:route, :schema, :client_ip, :actor)
|
|
66
70
|
|
|
67
71
|
def updated_settings(known_config_generation = nil, known_firewall_lists_generation = nil)
|
|
@@ -92,6 +96,13 @@ module Aikido::Zen::WorkerProcess
|
|
|
92
96
|
route = Aikido::Zen::Route.from_json(route_data)
|
|
93
97
|
Aikido::Zen.rate_limiter.calculate_rate_limits(RequestKind.new(route, nil, ip, actor))
|
|
94
98
|
end
|
|
99
|
+
|
|
100
|
+
# @return [Array<Hash>, nil]
|
|
101
|
+
def record_attack_wave(client_ip, verb, path)
|
|
102
|
+
sample = Aikido::Zen::AttackWave::Sample.new(verb: verb, path: path)
|
|
103
|
+
samples = @detector.record(client_ip, sample)
|
|
104
|
+
samples&.map(&:as_json)
|
|
105
|
+
end
|
|
95
106
|
end
|
|
96
107
|
end
|
|
97
108
|
end
|
data/lib/aikido/zen.rb
CHANGED
|
@@ -153,6 +153,23 @@ module Aikido
|
|
|
153
153
|
Fiber.current.aikido_current_context = context
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
+
# @return [Boolean] whether the current request is bypassed.
|
|
157
|
+
def self.request_bypassed?
|
|
158
|
+
context = current_context
|
|
159
|
+
return unless context
|
|
160
|
+
|
|
161
|
+
return context.request_bypassed unless context.request_bypassed.nil?
|
|
162
|
+
|
|
163
|
+
context.request_bypassed = runtime_settings.bypassed_ip?(context.request.client_ip)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Marks the current request as bypassed.
|
|
167
|
+
#
|
|
168
|
+
# @return [void]
|
|
169
|
+
def self.request_bypassed!
|
|
170
|
+
current_context&.request_bypassed = true
|
|
171
|
+
end
|
|
172
|
+
|
|
156
173
|
# Track statistics about an HTTP request the app is handling.
|
|
157
174
|
#
|
|
158
175
|
# @param request [Aikido::Zen::Request]
|
|
@@ -249,6 +266,49 @@ module Aikido
|
|
|
249
266
|
@attack_wave_detector ||= AttackWave::Detector.new
|
|
250
267
|
end
|
|
251
268
|
|
|
269
|
+
# Classifies the current request and, if it is classified as suspicious,
|
|
270
|
+
# records a sample against the attack wave detector.
|
|
271
|
+
#
|
|
272
|
+
# @param context [Aikido::Zen::Context]
|
|
273
|
+
# @param status_code [Integer, nil]
|
|
274
|
+
# @return [Array<Aikido::Zen::AttackWave::Sample>, nil]
|
|
275
|
+
def self.detect_attack_wave(context, status_code = nil)
|
|
276
|
+
client_ip = context.request.client_ip
|
|
277
|
+
return nil unless client_ip
|
|
278
|
+
|
|
279
|
+
return nil unless AttackWave::Helpers.web_scanner?(context, status_code)
|
|
280
|
+
|
|
281
|
+
record_attack_wave(client_ip, AttackWave::Helpers.sample_for(context))
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Records a suspicious sample and, if the threshold for triggering
|
|
285
|
+
# an attack wave has been crossed, flags the client IP as having
|
|
286
|
+
# just triggered an attack wave.
|
|
287
|
+
#
|
|
288
|
+
# In multiprocess deployments, on RPC failure, the worker process
|
|
289
|
+
# records against its local detector.
|
|
290
|
+
#
|
|
291
|
+
# If RPC failures are intermittent, it is possible that an attack
|
|
292
|
+
# wave may be missed or duplicated, because state is split between
|
|
293
|
+
# the global and local detectors.
|
|
294
|
+
#
|
|
295
|
+
# @param client_ip [String]
|
|
296
|
+
# @param sample [Aikido::Zen::AttackWave::Sample]
|
|
297
|
+
# @return [Array<Aikido::Zen::AttackWave::Sample>, nil]
|
|
298
|
+
def self.record_attack_wave(client_ip, sample)
|
|
299
|
+
worker_process_client = @worker_process_client
|
|
300
|
+
|
|
301
|
+
if worker_process_client
|
|
302
|
+
begin
|
|
303
|
+
worker_process_client.record_attack_wave(client_ip, sample)
|
|
304
|
+
rescue
|
|
305
|
+
attack_wave_detector.record(client_ip, sample)
|
|
306
|
+
end
|
|
307
|
+
else
|
|
308
|
+
attack_wave_detector.record(client_ip, sample)
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
252
312
|
# @return [Aikido::Zen::IDOR::Protector]
|
|
253
313
|
def self.idor_protector
|
|
254
314
|
@idor_protector ||= IDOR::Protector.new
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aikido-zen
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.7.2
|
|
4
|
+
version: 1.7.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aikido Security
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -93,6 +93,7 @@ files:
|
|
|
93
93
|
- docs/invalid-sql-queries.md
|
|
94
94
|
- docs/proxy.md
|
|
95
95
|
- docs/rails.md
|
|
96
|
+
- docs/request-bypassing.md
|
|
96
97
|
- docs/troubleshooting.md
|
|
97
98
|
- lib/aikido-zen.rb
|
|
98
99
|
- lib/aikido/zen.rb
|