aikido-zen 1.7.2 → 1.9.0.beta.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/README.md +8 -1
- data/benchmarks/ip_list/benchmark.rb +1 -1
- data/docs/custom-event-tracking.md +33 -0
- data/docs/request-bypassing.md +16 -4
- data/lib/aikido/zen/agent.rb +35 -16
- data/lib/aikido/zen/api_client.rb +2 -0
- data/lib/aikido/zen/attack_wave/helpers.rb +0 -1
- data/lib/aikido/zen/collector/event.rb +12 -6
- 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 -6
- data/lib/aikido/zen/config.rb +27 -5
- data/lib/aikido/zen/event.rb +26 -0
- data/lib/aikido/zen/{runtime_settings → firewall}/ip_list.rb +1 -1
- data/lib/aikido/zen/firewall.rb +132 -0
- data/lib/aikido/zen/middleware/fork_detector.rb +10 -4
- data/lib/aikido/zen/middleware/ip_list_checker.rb +5 -5
- data/lib/aikido/zen/middleware/user_agent_checker.rb +6 -6
- data/lib/aikido/zen/runtime_settings.rb +1 -150
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +5 -1
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +37 -2
- data/lib/aikido/zen/sink.rb +8 -3
- data/lib/aikido/zen/sinks/async_http.rb +2 -4
- data/lib/aikido/zen/sinks/curb.rb +2 -4
- data/lib/aikido/zen/sinks/em_http.rb +2 -4
- data/lib/aikido/zen/sinks/excon.rb +2 -4
- data/lib/aikido/zen/sinks/file.rb +1 -1
- data/lib/aikido/zen/sinks/http.rb +2 -4
- data/lib/aikido/zen/sinks/httpclient.rb +2 -4
- data/lib/aikido/zen/sinks/httpx.rb +2 -4
- 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 +2 -4
- data/lib/aikido/zen/sinks/patron.rb +2 -4
- 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 +11 -9
- 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 +2 -4
- data/lib/aikido/zen/version.rb +1 -1
- data/lib/aikido/zen/worker_process/agent/client.rb +2 -2
- data/lib/aikido/zen.rb +83 -9
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d41298c1b3dc0135c211b4beade2a723603cef96372b5526447e5a0d792d1bb9
|
|
4
|
+
data.tar.gz: 247dea5c7895db42823c445ce7e0f32995ba833cecbcf7430ad09f33c897c179
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ada3f8112966d020346b164380a2cbff9e1ba7c68cfdde2ab2a440d41e1c241c6ef73c9214597fd004e408b61c00ecc9b78294bfd73bef0effd1ca91dbdee5b4
|
|
7
|
+
data.tar.gz: 7f61d126b05f54339ca7633e8058e36d298dcd0a4b0abf93eebe4abb96a5c66edbf0e56cc3e6457ed92f8cf46231d6f00f60dc5b8487bdd014532db48ef52dba
|
data/README.md
CHANGED
|
@@ -39,6 +39,12 @@ Zen for Ruby 2.7+ is compatible with:
|
|
|
39
39
|
|
|
40
40
|
* ✅ [Ruby on Rails](docs/rails.md) 7.x, 8.x
|
|
41
41
|
|
|
42
|
+
### Application servers
|
|
43
|
+
|
|
44
|
+
* ✅ [Puma](https://puma.io/)
|
|
45
|
+
|
|
46
|
+
Our test suite only covers Puma. If you use another Rack server, [contact us](docs/troubleshooting.md#contact-support) and we'll help check whether Zen is compatible.
|
|
47
|
+
|
|
42
48
|
### Database drivers
|
|
43
49
|
|
|
44
50
|
* ✅ [`sqlite3`](https://github.com/sparklemotion/sqlite3-ruby) 1.x, 2.x
|
|
@@ -117,7 +123,8 @@ See [Reporting to Aikido](#reporting-to-your-aikido-security-dashboard) to learn
|
|
|
117
123
|
|
|
118
124
|
## Additional configuration
|
|
119
125
|
|
|
120
|
-
[Configure Zen using environment variables for authentication, mode settings, debugging, and more
|
|
126
|
+
* [Configure Zen using environment variables for authentication, mode settings, debugging, and more](https://help.aikido.dev/doc/configuration-via-env-vars/docrSItUkeR9)
|
|
127
|
+
* [Track custom events](docs/custom-event-tracking.md): trigger events that Playbooks can act on
|
|
121
128
|
|
|
122
129
|
## License
|
|
123
130
|
|
|
@@ -24,7 +24,7 @@ end
|
|
|
24
24
|
if __FILE__ == $0
|
|
25
25
|
ip_ranges = random_ip_ranges.take(1000)
|
|
26
26
|
|
|
27
|
-
ip_list = Aikido::Zen::
|
|
27
|
+
ip_list = Aikido::Zen::Firewall::IPList.from_json({
|
|
28
28
|
"key" => "key",
|
|
29
29
|
"source" => "source",
|
|
30
30
|
"description" => "description",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Track custom events
|
|
2
|
+
|
|
3
|
+
Use `Aikido::Zen.track_custom_event` to report events that only your application knows about, such as failed logins. [Playbooks](https://help.aikido.dev/zen-firewall/zen-features/playbooks) can act when an event occurs repeatedly, for example by blocking an IP after three failed logins in five minutes.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
# app/controllers/application_controller.rb
|
|
7
|
+
class ApplicationController < ActionController::Base
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def authenticate_user!
|
|
11
|
+
# Your authentication logic here
|
|
12
|
+
# ...
|
|
13
|
+
|
|
14
|
+
unless current_user
|
|
15
|
+
Aikido::Zen.track_custom_event("user.login_failed")
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Aikido::Zen.set_user(
|
|
20
|
+
id: current_user.id,
|
|
21
|
+
name: current_user.name
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
Aikido::Zen.track_custom_event("user.login_succeeded")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
After adding `Aikido::Zen.track_custom_event`, trigger the event at least once. It will then appear on the Playbooks page in the Aikido dashboard. From there, you can create a playbook and choose what should happen when the event occurs. Calling `Aikido::Zen.track_custom_event` by itself does not create a playbook or block anything.
|
|
30
|
+
|
|
31
|
+
Call `Aikido::Zen.track_custom_event` while handling an HTTP request. Zen associates the event with the request's IP address. Playbook counts are per IP, not across your whole app. If you call [`Aikido::Zen.set_user`](./rails.md#rate-limiting-and-user-blocking) before tracking the event, Zen also includes the current user. `Aikido::Zen.set_user` is optional. Events without a user are still tracked.
|
|
32
|
+
|
|
33
|
+
Event names can use any format. We recommend lowercase, dot-separated names such as `user.login_failed`.
|
data/docs/request-bypassing.md
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
# Bypass Zen for a specific request
|
|
2
2
|
|
|
3
|
-
|
|
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
4
|
|
|
5
5
|
> [!NOTE]
|
|
6
|
-
> Zen
|
|
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
7
|
|
|
8
|
-
|
|
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:
|
|
9
21
|
|
|
10
22
|
```ruby
|
|
11
23
|
# config/application.rb
|
|
@@ -35,4 +47,4 @@ end
|
|
|
35
47
|
```
|
|
36
48
|
|
|
37
49
|
> [!WARNING]
|
|
38
|
-
>
|
|
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
|
@@ -68,27 +68,22 @@ module Aikido::Zen
|
|
|
68
68
|
# :nocov:
|
|
69
69
|
|
|
70
70
|
report(Events::Started.new(time: @started_at)) do |response|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if @config.realtime_settings_updates_enabled? || @settings.realtime_settings_updates_enabled?
|
|
75
|
-
@api_stream.handle("config-updated") do |event|
|
|
76
|
-
@config.logger.debug("Received server-sent event: config-updated")
|
|
77
|
-
settings_updated(event)
|
|
78
|
-
end
|
|
71
|
+
update_settings_from_runtime_config!(response, reason: "after start")
|
|
72
|
+
updated_settings!
|
|
79
73
|
|
|
80
|
-
|
|
74
|
+
if @config.realtime_settings_updates_enabled? || @settings.realtime_settings_updates_enabled?
|
|
75
|
+
@api_stream.handle("config-updated") do |event|
|
|
76
|
+
@config.logger.debug("Received server-sent event: config-updated")
|
|
77
|
+
settings_updated(event)
|
|
81
78
|
end
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# empty
|
|
79
|
+
|
|
80
|
+
@api_stream.start!
|
|
85
81
|
end
|
|
86
|
-
# :nocov:
|
|
87
82
|
rescue => err
|
|
88
83
|
@config.logger.error(err.message)
|
|
89
84
|
end
|
|
90
85
|
|
|
91
|
-
|
|
86
|
+
@worker.perform do
|
|
92
87
|
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after start")
|
|
93
88
|
rescue => err
|
|
94
89
|
@config.logger.error(err.message)
|
|
@@ -165,6 +160,15 @@ module Aikido::Zen
|
|
|
165
160
|
end
|
|
166
161
|
end
|
|
167
162
|
|
|
163
|
+
# Reports a custom event tracked via Aikido::Zen.track_custom_event, if
|
|
164
|
+
# reporting is enabled.
|
|
165
|
+
#
|
|
166
|
+
# @param event [Aikido::Zen::Events::Custom]
|
|
167
|
+
# @return [void]
|
|
168
|
+
def report_custom_event(event)
|
|
169
|
+
report(event) if @api_client.can_make_requests?
|
|
170
|
+
end
|
|
171
|
+
|
|
168
172
|
# @api private
|
|
169
173
|
#
|
|
170
174
|
# Atomically flushes all the stats stored by the agent, and sends a
|
|
@@ -220,6 +224,8 @@ module Aikido::Zen
|
|
|
220
224
|
updated_at = Time.at(event[:data]["configUpdatedAt"].to_i)
|
|
221
225
|
|
|
222
226
|
if should_fetch_settings?(updated_at)
|
|
227
|
+
return if realtime_settings_updates_too_fast?
|
|
228
|
+
|
|
223
229
|
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config, reason: "after server-sent event")
|
|
224
230
|
updated_settings!
|
|
225
231
|
|
|
@@ -239,6 +245,19 @@ module Aikido::Zen
|
|
|
239
245
|
updated_at > last_updated_at
|
|
240
246
|
end
|
|
241
247
|
|
|
248
|
+
def realtime_settings_updates_too_fast?
|
|
249
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
250
|
+
|
|
251
|
+
last_updated_at = @realtime_settings_updates_last_updated_at
|
|
252
|
+
if last_updated_at && now - last_updated_at < @config.realtime_settings_updates_min_time_between_events
|
|
253
|
+
return true
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
@realtime_settings_updates_last_updated_at = now
|
|
257
|
+
|
|
258
|
+
false
|
|
259
|
+
end
|
|
260
|
+
|
|
242
261
|
def heartbeats
|
|
243
262
|
@heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
|
|
244
263
|
config: @config,
|
|
@@ -257,7 +276,7 @@ module Aikido::Zen
|
|
|
257
276
|
begin
|
|
258
277
|
return false unless Aikido::Zen.api_cache.update_runtime_config(data)
|
|
259
278
|
|
|
260
|
-
if Aikido::Zen.runtime_settings.
|
|
279
|
+
if Aikido::Zen.runtime_settings.update_from_json(data)
|
|
261
280
|
@config.logger.info("Updated runtime settings #{reason}")
|
|
262
281
|
true
|
|
263
282
|
else
|
|
@@ -279,7 +298,7 @@ module Aikido::Zen
|
|
|
279
298
|
begin
|
|
280
299
|
return false unless Aikido::Zen.api_cache.update_runtime_firewall_lists(data)
|
|
281
300
|
|
|
282
|
-
if Aikido::Zen.
|
|
301
|
+
if Aikido::Zen.firewall.update_from_json(data)
|
|
283
302
|
@config.logger.info("Updated runtime firewall list #{reason}")
|
|
284
303
|
true
|
|
285
304
|
else
|
|
@@ -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
|
|
|
@@ -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
|
|
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.
|
|
@@ -56,6 +56,10 @@ module Aikido::Zen
|
|
|
56
56
|
# each initial heartbeat event.
|
|
57
57
|
attr_accessor :initial_heartbeat_delays
|
|
58
58
|
|
|
59
|
+
# @return [Symbol] the agent mode for forked worker processes. Can be set
|
|
60
|
+
# through the AIKIDO_AGENT_MODE environment variable.
|
|
61
|
+
attr_reader :agent_mode
|
|
62
|
+
|
|
59
63
|
# @return [Integer] the interval in seconds at which forked worker processes
|
|
60
64
|
# poll the parent process for updated runtime settings. Defaults to 10 seconds.
|
|
61
65
|
attr_accessor :worker_process_polling_interval
|
|
@@ -229,6 +233,11 @@ module Aikido::Zen
|
|
|
229
233
|
attr_accessor :realtime_settings_updates_enabled
|
|
230
234
|
alias_method :realtime_settings_updates_enabled?, :realtime_settings_updates_enabled
|
|
231
235
|
|
|
236
|
+
# @return [Integer] the minimum time in milliseconds between realtime
|
|
237
|
+
# settings updates.
|
|
238
|
+
# Defaults to 9 seconds in milliseconds.
|
|
239
|
+
attr_accessor :realtime_settings_updates_min_time_between_events
|
|
240
|
+
|
|
232
241
|
def initialize
|
|
233
242
|
self.insert_middleware_after = ::ActionDispatch::RemoteIp
|
|
234
243
|
self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
|
|
@@ -236,10 +245,11 @@ module Aikido::Zen
|
|
|
236
245
|
self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
|
|
237
246
|
self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
|
|
238
247
|
self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT") { regional_api_endpoint(api_token) }
|
|
239
|
-
self.realtime_endpoint =
|
|
248
|
+
self.realtime_endpoint = api_endpoint
|
|
240
249
|
self.api_timeouts = 10
|
|
241
250
|
self.polling_interval = 60 # 1 min
|
|
242
251
|
self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
|
|
252
|
+
self.agent_mode = ENV.fetch("AIKIDO_AGENT_MODE", "shared")
|
|
243
253
|
self.worker_process_polling_interval = 10
|
|
244
254
|
self.worker_process_polling_jitter = 10
|
|
245
255
|
self.worker_process_heartbeat_interval = 10
|
|
@@ -277,6 +287,7 @@ module Aikido::Zen
|
|
|
277
287
|
self.idor_excluded_table_names = []
|
|
278
288
|
self.idor_max_cache_entries = 1000
|
|
279
289
|
self.realtime_settings_updates_enabled = false
|
|
290
|
+
self.realtime_settings_updates_min_time_between_events = 9 * 1000 # 9 sec (ms)
|
|
280
291
|
end
|
|
281
292
|
|
|
282
293
|
# Set the base URL for API requests.
|
|
@@ -293,6 +304,17 @@ module Aikido::Zen
|
|
|
293
304
|
@realtime_endpoint = URI(url)
|
|
294
305
|
end
|
|
295
306
|
|
|
307
|
+
# @param mode [Symbol, String] a supported agent mode.
|
|
308
|
+
# @raise [ArgumentError]
|
|
309
|
+
def agent_mode=(mode)
|
|
310
|
+
mode = mode.to_sym
|
|
311
|
+
unless AGENT_MODES.include?(mode)
|
|
312
|
+
raise ArgumentError, "agent_mode must be one of #{AGENT_MODES.join(", ")}, got #{mode}"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
@agent_mode = mode
|
|
316
|
+
end
|
|
317
|
+
|
|
296
318
|
# Set the logger and configure its severity level according to agent's debug mode
|
|
297
319
|
# @param logger [::Logger]
|
|
298
320
|
def logger=(logger)
|
|
@@ -364,6 +386,9 @@ module Aikido::Zen
|
|
|
364
386
|
end
|
|
365
387
|
end
|
|
366
388
|
|
|
389
|
+
# @!visibility private
|
|
390
|
+
AGENT_MODES = [:shared, :per_worker].freeze
|
|
391
|
+
|
|
367
392
|
# @!visibility private
|
|
368
393
|
DEFAULT_AIKIDO_ENDPOINT = "https://guard.aikido.dev"
|
|
369
394
|
|
|
@@ -377,9 +402,6 @@ module Aikido::Zen
|
|
|
377
402
|
"AU" => "https://guard.au.aikido.dev"
|
|
378
403
|
}.freeze
|
|
379
404
|
|
|
380
|
-
# @!visibility private
|
|
381
|
-
DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
|
|
382
|
-
|
|
383
405
|
# @!visibility private
|
|
384
406
|
DEFAULT_JSON_ENCODER = JSON.method(:dump)
|
|
385
407
|
|
data/lib/aikido/zen/event.rb
CHANGED
|
@@ -95,5 +95,31 @@ module Aikido::Zen
|
|
|
95
95
|
)
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
|
+
|
|
99
|
+
# Event sent by Aikido::Zen.track_custom_event to record a custom,
|
|
100
|
+
# user-named event happening during an HTTP request.
|
|
101
|
+
class Custom < Event
|
|
102
|
+
# @param name [String] the name of the tracked event.
|
|
103
|
+
# @param request [Aikido::Zen::Request]
|
|
104
|
+
# @param user [Aikido::Zen::Actor, nil]
|
|
105
|
+
# @param opts [Hash<Symbol, Object>] any other options to pass to
|
|
106
|
+
# the superclass initializer.
|
|
107
|
+
def initialize(name:, request:, user: nil, **opts)
|
|
108
|
+
super(type: "custom", **opts)
|
|
109
|
+
@name = name
|
|
110
|
+
@request = request
|
|
111
|
+
@user = user
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def as_json
|
|
115
|
+
super.update(
|
|
116
|
+
{
|
|
117
|
+
name: @name,
|
|
118
|
+
request: @request.as_json,
|
|
119
|
+
user: @user && {id: @user.id, name: @user.name}.compact
|
|
120
|
+
}.compact
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
98
124
|
end
|
|
99
125
|
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen
|
|
4
|
+
class Firewall
|
|
5
|
+
# @return [Regexp, nil]
|
|
6
|
+
attr_accessor :blocked_user_agent_regexp
|
|
7
|
+
|
|
8
|
+
# @return [Regexp, nil]
|
|
9
|
+
attr_accessor :monitored_user_agent_regexp
|
|
10
|
+
|
|
11
|
+
# @return [Array<Hash>, nil]
|
|
12
|
+
attr_accessor :user_agent_details
|
|
13
|
+
|
|
14
|
+
# @return [Array<Aikido::Zen::Firewall::IPList>]
|
|
15
|
+
attr_accessor :blocked_ip_lists
|
|
16
|
+
|
|
17
|
+
# @return [Array<Aikido::Zen::Firewall::IPList>]
|
|
18
|
+
attr_accessor :allowed_ip_lists
|
|
19
|
+
|
|
20
|
+
# @return [Array<Aikido::Zen::Firewall::IPList>]
|
|
21
|
+
attr_accessor :monitored_ip_lists
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
self.blocked_ip_lists = []
|
|
25
|
+
self.allowed_ip_lists = []
|
|
26
|
+
self.monitored_ip_lists = []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param data [Hash] the decoded JSON payload from /api/runtime/firewall/lists
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def update_from_json(data)
|
|
32
|
+
self.blocked_user_agent_regexp = pattern(data["blockedUserAgents"])
|
|
33
|
+
|
|
34
|
+
self.monitored_user_agent_regexp = pattern(data["monitoredUserAgents"])
|
|
35
|
+
|
|
36
|
+
self.user_agent_details = []
|
|
37
|
+
|
|
38
|
+
data["userAgentDetails"]&.each do |record|
|
|
39
|
+
key = record["key"]
|
|
40
|
+
pattern = pattern(record["pattern"])
|
|
41
|
+
|
|
42
|
+
next if key.nil? || pattern.nil?
|
|
43
|
+
|
|
44
|
+
user_agent_details << {
|
|
45
|
+
key: key,
|
|
46
|
+
pattern: pattern
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
self.blocked_ip_lists = []
|
|
51
|
+
|
|
52
|
+
data["blockedIPAddresses"]&.each do |ip_list|
|
|
53
|
+
blocked_ip_lists << Firewall::IPList.from_json(ip_list)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
self.allowed_ip_lists = []
|
|
57
|
+
|
|
58
|
+
data["allowedIPAddresses"]&.each do |ip_list|
|
|
59
|
+
allowed_ip_lists << Firewall::IPList.from_json(ip_list)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
self.monitored_ip_lists = []
|
|
63
|
+
|
|
64
|
+
data["monitoredIPAddresses"]&.each do |ip_list|
|
|
65
|
+
monitored_ip_lists << Firewall::IPList.from_json(ip_list)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Construct a regular expression from the non-nil and non-empty string,
|
|
72
|
+
# otherwise return nil.
|
|
73
|
+
#
|
|
74
|
+
# The resulting regular expression is case insensitive.
|
|
75
|
+
#
|
|
76
|
+
# @param string [String, nil]
|
|
77
|
+
# @return [Regexp, nil]
|
|
78
|
+
private def pattern(string)
|
|
79
|
+
return nil if string.nil? || string.empty?
|
|
80
|
+
|
|
81
|
+
begin
|
|
82
|
+
/#{string}/i
|
|
83
|
+
rescue RegexpError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param user_agent [String] the user agent
|
|
89
|
+
# @return [Boolean] whether the user agent should be blocked
|
|
90
|
+
def blocked_user_agent?(user_agent)
|
|
91
|
+
return false if blocked_user_agent_regexp.nil?
|
|
92
|
+
|
|
93
|
+
blocked_user_agent_regexp.match?(user_agent)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @param user_agent [String] the user agent
|
|
97
|
+
# @return [Boolean] whether the user agent should be monitored
|
|
98
|
+
def monitored_user_agent?(user_agent)
|
|
99
|
+
return false if monitored_user_agent_regexp.nil?
|
|
100
|
+
|
|
101
|
+
monitored_user_agent_regexp.match?(user_agent)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @param user_agent [String] the user agent
|
|
105
|
+
# @return [Array<String>] the matching user agent keys
|
|
106
|
+
def user_agent_keys(user_agent)
|
|
107
|
+
return [] if user_agent_details.nil?
|
|
108
|
+
|
|
109
|
+
user_agent_details.filter_map { |record| record[:key] if record[:pattern].match?(user_agent) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def allowed_ip?(ip)
|
|
113
|
+
allowed_ip_lists.empty? || allowed_ip_lists.any? { |ip_list| ip_list.include?(ip) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def blocked_ip?(ip)
|
|
117
|
+
blocked_ip_lists.any? { |ip_list| ip_list.include?(ip) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def monitored_ip?(ip)
|
|
121
|
+
monitored_ip_lists.any? { |ip_list| ip_list.include?(ip) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def monitored_ip_list_keys(ip)
|
|
125
|
+
return [] if ip.nil?
|
|
126
|
+
|
|
127
|
+
monitored_ip_lists.filter_map { |ip_list| ip_list.key if ip_list.include?(ip) }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
require_relative "firewall/ip_list"
|
|
@@ -11,14 +11,20 @@ module Aikido
|
|
|
11
11
|
@app = app
|
|
12
12
|
|
|
13
13
|
@pid = Concurrent::AtomicFixnum.new(Process.pid)
|
|
14
|
+
@fork_mutex = Mutex.new
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def call(env)
|
|
17
|
-
|
|
18
|
-
old_pid = @pid.value
|
|
18
|
+
pid = Process.pid
|
|
19
19
|
|
|
20
|
-
if
|
|
21
|
-
|
|
20
|
+
if pid != @pid.value
|
|
21
|
+
@fork_mutex.synchronize do
|
|
22
|
+
if pid != @pid.value
|
|
23
|
+
Aikido::Zen.fork!
|
|
24
|
+
|
|
25
|
+
@pid.value = pid
|
|
26
|
+
end
|
|
27
|
+
end
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
@app.call(env)
|