aikido-zen 1.7.2.beta.1-aarch64-linux → 1.8.0-aarch64-linux
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 +50 -0
- data/lib/aikido/zen/agent.rb +15 -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/event.rb +12 -6
- data/lib/aikido/zen/collector/routes.rb +2 -2
- data/lib/aikido/zen/collector/sink_stats.rb +4 -0
- data/lib/aikido/zen/collector/stats.rb +6 -2
- data/lib/aikido/zen/collector.rb +6 -11
- data/lib/aikido/zen/config.rb +8 -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/sink.rb +8 -3
- data/lib/aikido/zen/sinks/action_controller.rb +1 -1
- data/lib/aikido/zen/sinks/async_http.rb +3 -7
- data/lib/aikido/zen/sinks/curb.rb +3 -7
- data/lib/aikido/zen/sinks/em_http.rb +3 -7
- data/lib/aikido/zen/sinks/excon.rb +3 -7
- data/lib/aikido/zen/sinks/file.rb +1 -1
- data/lib/aikido/zen/sinks/http.rb +3 -7
- data/lib/aikido/zen/sinks/httpclient.rb +3 -7
- data/lib/aikido/zen/sinks/httpx.rb +3 -7
- data/lib/aikido/zen/sinks/kernel.rb +1 -1
- data/lib/aikido/zen/sinks/mysql2.rb +1 -1
- data/lib/aikido/zen/sinks/net_http.rb +3 -7
- data/lib/aikido/zen/sinks/patron.rb +3 -7
- data/lib/aikido/zen/sinks/pg.rb +1 -1
- data/lib/aikido/zen/sinks/resolv.rb +1 -1
- data/lib/aikido/zen/sinks/socket.rb +1 -1
- data/lib/aikido/zen/sinks/sqlite3.rb +1 -1
- data/lib/aikido/zen/sinks/trilogy.rb +1 -1
- data/lib/aikido/zen/sinks/typhoeus.rb +3 -7
- 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 +68 -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: c4c9820f5bed416d8a88ff5620690ff8a2dea5f404f967e6b1650562c05cb2cc
|
|
4
|
+
data.tar.gz: b08ee75b98ecd70862a4352d2a57713877d9ffaec6ba6820470c915c99169a12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35f2cba1d894bb9b2e5048b2fcbd32cde9691c2236befc958e44f77f4a46f7b539d0b336d54ace283086f1d1f85ad3c0609af9df20d662088f8feeb5db8ebe2b
|
|
7
|
+
data.tar.gz: 18d284076efbb1b74c60ffd7a2e9bcb59a201a27941171027a31a289794a3a35362c5ba85113e35d682b7831b996c5317b58a8ee184d6342429c4dbfa80ab609
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Bypass Zen for a specific request
|
|
2
|
+
|
|
3
|
+
Call `Aikido::Zen.request_bypassed!` from a Rack middleware to bypass Zen for this request. A bypassed request is fully excluded from Zen inspection and enforcement: Zen will not analyze the request, generate findings, or apply blocking rules for that traffic. Your application handles the request normally.
|
|
4
|
+
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> Zen's built-in [Bypassed IPs](https://help.aikido.dev/zen-firewall/zen-features/bypassed-ips) feature uses request bypassing internally, triggered by a matching IP/CIDR. `Aikido::Zen.request_bypassed!` lets you bypass requests using your custom logic.
|
|
7
|
+
|
|
8
|
+
## What gets bypassed
|
|
9
|
+
|
|
10
|
+
* **Attack protection** — SQL injection, path traversal, command injection, and SSRF attacks are not blocked or reported
|
|
11
|
+
* **Rate limiting** — never triggered.
|
|
12
|
+
* **IP blocking** — Known Threat Actors, Tor traffic blocking/monitoring, country blocking, and custom IP allow/block lists are not checked.
|
|
13
|
+
* **Bot traffic blocking** — not checked.
|
|
14
|
+
* **User blocking** — blocked users are not blocked.
|
|
15
|
+
* **Statistics** — the request isn't counted, and doesn't count against your monitored request quota.
|
|
16
|
+
* **Attack wave protection** — the request doesn't count towards wave detection.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
Insert your middleware directly after `Aikido::Zen::Middleware::ContextSetter`, so it runs after the request context is set up but before Zen's other middleware:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# config/application.rb
|
|
24
|
+
class RequestBypasser
|
|
25
|
+
def initialize(app)
|
|
26
|
+
@app = app
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(env)
|
|
30
|
+
if your_custom_logic?(env)
|
|
31
|
+
Aikido::Zen.request_bypassed! # Bypasses Zen for this request
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@app.call(env)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module YourApp
|
|
39
|
+
class Application < Rails::Application
|
|
40
|
+
# ...
|
|
41
|
+
|
|
42
|
+
initializer "your_app.insert_request_bypasser", after: "aikido.add_middleware" do |app|
|
|
43
|
+
app.middleware.insert_after Aikido::Zen::Middleware::ContextSetter, RequestBypasser
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> [!WARNING]
|
|
50
|
+
> A bypassed request gets zero protection from Zen — no attack detection, no rate limiting, no blocking, no tracking. Ensure that your custom logic only bypasses requests that you fully trust.
|
data/lib/aikido/zen/agent.rb
CHANGED
|
@@ -220,6 +220,8 @@ module Aikido::Zen
|
|
|
220
220
|
updated_at = Time.at(event[:data]["configUpdatedAt"].to_i)
|
|
221
221
|
|
|
222
222
|
if should_fetch_settings?(updated_at)
|
|
223
|
+
return if realtime_settings_updates_too_fast?
|
|
224
|
+
|
|
223
225
|
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config, reason: "after server-sent event")
|
|
224
226
|
updated_settings!
|
|
225
227
|
|
|
@@ -239,6 +241,19 @@ module Aikido::Zen
|
|
|
239
241
|
updated_at > last_updated_at
|
|
240
242
|
end
|
|
241
243
|
|
|
244
|
+
def realtime_settings_updates_too_fast?
|
|
245
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
246
|
+
|
|
247
|
+
last_updated_at = @realtime_settings_updates_last_updated_at
|
|
248
|
+
if last_updated_at && now - last_updated_at < @config.realtime_settings_updates_min_time_between_events
|
|
249
|
+
return true
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
@realtime_settings_updates_last_updated_at = now
|
|
253
|
+
|
|
254
|
+
false
|
|
255
|
+
end
|
|
256
|
+
|
|
242
257
|
def heartbeats
|
|
243
258
|
@heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
|
|
244
259
|
config: @config,
|
|
@@ -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
|
|
@@ -157,14 +157,16 @@ module Aikido::Zen
|
|
|
157
157
|
def self.from_json(data)
|
|
158
158
|
new(
|
|
159
159
|
data["sink_name"],
|
|
160
|
+
data["sink_kind"],
|
|
160
161
|
data["duration"],
|
|
161
162
|
has_errors: data["has_errors"]
|
|
162
163
|
)
|
|
163
164
|
end
|
|
164
165
|
|
|
165
|
-
def initialize(sink_name, duration, has_errors:)
|
|
166
|
+
def initialize(sink_name, sink_kind, duration, has_errors:)
|
|
166
167
|
super()
|
|
167
168
|
@sink_name = sink_name
|
|
169
|
+
@sink_kind = sink_kind
|
|
168
170
|
@duration = duration
|
|
169
171
|
@has_errors = has_errors
|
|
170
172
|
end
|
|
@@ -172,17 +174,18 @@ module Aikido::Zen
|
|
|
172
174
|
def as_json
|
|
173
175
|
super.update({
|
|
174
176
|
"sink_name" => @sink_name,
|
|
177
|
+
"sink_kind" => @sink_kind,
|
|
175
178
|
"duration" => @duration,
|
|
176
179
|
"has_errors" => @has_errors
|
|
177
180
|
})
|
|
178
181
|
end
|
|
179
182
|
|
|
180
183
|
def handle(collector)
|
|
181
|
-
collector.handle_track_scan(@sink_name, @duration, has_errors: @has_errors)
|
|
184
|
+
collector.handle_track_scan(@sink_name, @sink_kind, @duration, has_errors: @has_errors)
|
|
182
185
|
end
|
|
183
186
|
|
|
184
187
|
def inspect
|
|
185
|
-
"#<#{self.class.name} #{@sink_name} #{format "%0.6f", @duration} #{@has_errors}>"
|
|
188
|
+
"#<#{self.class.name} #{@sink_name} #{@sink_kind} #{format "%0.6f", @duration} #{@has_errors}>"
|
|
186
189
|
end
|
|
187
190
|
end
|
|
188
191
|
|
|
@@ -192,29 +195,32 @@ module Aikido::Zen
|
|
|
192
195
|
def self.from_json(data)
|
|
193
196
|
new(
|
|
194
197
|
data["sink_name"],
|
|
198
|
+
data["sink_kind"],
|
|
195
199
|
being_blocked: data["being_blocked"]
|
|
196
200
|
)
|
|
197
201
|
end
|
|
198
202
|
|
|
199
|
-
def initialize(sink_name, being_blocked:)
|
|
203
|
+
def initialize(sink_name, sink_kind, being_blocked:)
|
|
200
204
|
super()
|
|
201
205
|
@sink_name = sink_name
|
|
206
|
+
@sink_kind = sink_kind
|
|
202
207
|
@being_blocked = being_blocked
|
|
203
208
|
end
|
|
204
209
|
|
|
205
210
|
def as_json
|
|
206
211
|
super.update({
|
|
207
212
|
"sink_name" => @sink_name,
|
|
213
|
+
"sink_kind" => @sink_kind,
|
|
208
214
|
"being_blocked" => @being_blocked
|
|
209
215
|
})
|
|
210
216
|
end
|
|
211
217
|
|
|
212
218
|
def handle(collector)
|
|
213
|
-
collector.handle_track_attack(@sink_name, being_blocked: @being_blocked)
|
|
219
|
+
collector.handle_track_attack(@sink_name, @sink_kind, being_blocked: @being_blocked)
|
|
214
220
|
end
|
|
215
221
|
|
|
216
222
|
def inspect
|
|
217
|
-
"#<#{self.class.name} #{@sink_name} #{@being_blocked}>"
|
|
223
|
+
"#<#{self.class.name} #{@sink_name} #{@sink_kind} #{@being_blocked}>"
|
|
218
224
|
end
|
|
219
225
|
end
|
|
220
226
|
|
|
@@ -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
|
|
@@ -7,6 +7,9 @@ module Aikido::Zen
|
|
|
7
7
|
#
|
|
8
8
|
# Tracks data specific to a single Sink.
|
|
9
9
|
class Collector::SinkStats
|
|
10
|
+
# @return [String] category of operation this sink represents.
|
|
11
|
+
attr_accessor :kind
|
|
12
|
+
|
|
10
13
|
# @return [Integer] number of total calls to Sink#scan.
|
|
11
14
|
attr_accessor :scans
|
|
12
15
|
|
|
@@ -62,6 +65,7 @@ module Aikido::Zen
|
|
|
62
65
|
|
|
63
66
|
def as_json
|
|
64
67
|
{
|
|
68
|
+
"kind" => @kind,
|
|
65
69
|
"total" => @scans,
|
|
66
70
|
"interceptorThrewError" => @errors,
|
|
67
71
|
"withoutContext" => 0,
|
|
@@ -94,21 +94,25 @@ module Aikido::Zen
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
# @param sink_name [String] the name of the sink
|
|
97
|
+
# @param sink_kind [String] the category of operation the sink represents
|
|
97
98
|
# @param duration [Float] the length the scan in seconds
|
|
98
99
|
# @param has_errors [Boolean] whether errors occurred during the scan
|
|
99
100
|
# @return [void]
|
|
100
|
-
def add_scan(sink_name, duration, has_errors:)
|
|
101
|
+
def add_scan(sink_name, sink_kind, duration, has_errors:)
|
|
101
102
|
stats = @sinks[sink_name]
|
|
103
|
+
stats.kind = sink_kind
|
|
102
104
|
stats.scans += 1
|
|
103
105
|
stats.errors += 1 if has_errors
|
|
104
106
|
stats.add_timing(duration)
|
|
105
107
|
end
|
|
106
108
|
|
|
107
109
|
# @param sink_name [String] the name of the sink
|
|
110
|
+
# @param sink_kind [String] the category of operation the sink represents
|
|
108
111
|
# @param being_blocked [Boolean] whether the Agent blocked the request
|
|
109
112
|
# @return [void]
|
|
110
|
-
def add_attack(sink_name, being_blocked:)
|
|
113
|
+
def add_attack(sink_name, sink_kind, being_blocked:)
|
|
111
114
|
stats = @sinks[sink_name]
|
|
115
|
+
stats.kind = sink_kind
|
|
112
116
|
stats.attacks += 1
|
|
113
117
|
stats.blocked_attacks += 1 if being_blocked
|
|
114
118
|
end
|
data/lib/aikido/zen/collector.rb
CHANGED
|
@@ -146,12 +146,12 @@ module Aikido::Zen
|
|
|
146
146
|
# @param scan [Aikido::Zen::Scan]
|
|
147
147
|
# @return [void]
|
|
148
148
|
def track_scan(scan)
|
|
149
|
-
add_event(Events::TrackScan.new(scan.sink.name, scan.duration, has_errors: scan.errors?))
|
|
149
|
+
add_event(Events::TrackScan.new(scan.sink.name, scan.sink.kind, scan.duration, has_errors: scan.errors?))
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
-
def handle_track_scan(sink_name, duration, has_errors:)
|
|
152
|
+
def handle_track_scan(sink_name, sink_kind, duration, has_errors:)
|
|
153
153
|
synchronize(@stats) do |stats|
|
|
154
|
-
stats.add_scan(sink_name, duration, has_errors: has_errors)
|
|
154
|
+
stats.add_scan(sink_name, sink_kind, duration, has_errors: has_errors)
|
|
155
155
|
end
|
|
156
156
|
end
|
|
157
157
|
|
|
@@ -160,12 +160,12 @@ module Aikido::Zen
|
|
|
160
160
|
# @param attack [Aikido::Zen::Attack]
|
|
161
161
|
# @return [void]
|
|
162
162
|
def track_attack(attack)
|
|
163
|
-
add_event(Events::TrackAttack.new(attack.sink.name, being_blocked: attack.blocked?))
|
|
163
|
+
add_event(Events::TrackAttack.new(attack.sink.name, attack.sink.kind, being_blocked: attack.blocked?))
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
-
def handle_track_attack(sink_name, being_blocked:)
|
|
166
|
+
def handle_track_attack(sink_name, sink_kind, being_blocked:)
|
|
167
167
|
synchronize(@stats) do |stats|
|
|
168
|
-
stats.add_attack(sink_name, being_blocked: being_blocked)
|
|
168
|
+
stats.add_attack(sink_name, sink_kind, being_blocked: being_blocked)
|
|
169
169
|
end
|
|
170
170
|
end
|
|
171
171
|
|
|
@@ -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/config.rb
CHANGED
|
@@ -42,7 +42,7 @@ module Aikido::Zen
|
|
|
42
42
|
attr_reader :api_endpoint
|
|
43
43
|
|
|
44
44
|
# @return [URI] The HTTP host for the Aikido Runtime API. Defaults to
|
|
45
|
-
# +https://
|
|
45
|
+
# +https://guard.aikido.dev+.
|
|
46
46
|
attr_reader :realtime_endpoint
|
|
47
47
|
|
|
48
48
|
# @return [Hash] HTTP timeouts for communicating with the API.
|
|
@@ -229,6 +229,11 @@ module Aikido::Zen
|
|
|
229
229
|
attr_accessor :realtime_settings_updates_enabled
|
|
230
230
|
alias_method :realtime_settings_updates_enabled?, :realtime_settings_updates_enabled
|
|
231
231
|
|
|
232
|
+
# @return [Integer] the minimum time in milliseconds between realtime
|
|
233
|
+
# settings updates.
|
|
234
|
+
# Defaults to 9 seconds in milliseconds.
|
|
235
|
+
attr_accessor :realtime_settings_updates_min_time_between_events
|
|
236
|
+
|
|
232
237
|
def initialize
|
|
233
238
|
self.insert_middleware_after = ::ActionDispatch::RemoteIp
|
|
234
239
|
self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
|
|
@@ -236,7 +241,7 @@ module Aikido::Zen
|
|
|
236
241
|
self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
|
|
237
242
|
self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
|
|
238
243
|
self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT") { regional_api_endpoint(api_token) }
|
|
239
|
-
self.realtime_endpoint =
|
|
244
|
+
self.realtime_endpoint = api_endpoint
|
|
240
245
|
self.api_timeouts = 10
|
|
241
246
|
self.polling_interval = 60 # 1 min
|
|
242
247
|
self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
|
|
@@ -277,6 +282,7 @@ module Aikido::Zen
|
|
|
277
282
|
self.idor_excluded_table_names = []
|
|
278
283
|
self.idor_max_cache_entries = 1000
|
|
279
284
|
self.realtime_settings_updates_enabled = false
|
|
285
|
+
self.realtime_settings_updates_min_time_between_events = 9 * 1000 # 9 sec (ms)
|
|
280
286
|
end
|
|
281
287
|
|
|
282
288
|
# Set the base URL for API requests.
|
|
@@ -377,9 +383,6 @@ module Aikido::Zen
|
|
|
377
383
|
"AU" => "https://guard.au.aikido.dev"
|
|
378
384
|
}.freeze
|
|
379
385
|
|
|
380
|
-
# @!visibility private
|
|
381
|
-
DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
|
|
382
|
-
|
|
383
386
|
# @!visibility private
|
|
384
387
|
DEFAULT_JSON_ENCODER = JSON.method(:dump)
|
|
385
388
|
|
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
|
|