aikido-zen 1.8.0 → 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/lib/aikido/zen/agent.rb +20 -16
- data/lib/aikido/zen/api_client.rb +2 -0
- data/lib/aikido/zen/attack_wave/helpers.rb +0 -1
- data/lib/aikido/zen/config.rb +19 -0
- 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/sinks/socket.rb +10 -8
- data/lib/aikido/zen/version.rb +1 -1
- data/lib/aikido/zen/worker_process/agent/client.rb +2 -2
- data/lib/aikido/zen.rb +75 -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/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
|
|
@@ -272,7 +276,7 @@ module Aikido::Zen
|
|
|
272
276
|
begin
|
|
273
277
|
return false unless Aikido::Zen.api_cache.update_runtime_config(data)
|
|
274
278
|
|
|
275
|
-
if Aikido::Zen.runtime_settings.
|
|
279
|
+
if Aikido::Zen.runtime_settings.update_from_json(data)
|
|
276
280
|
@config.logger.info("Updated runtime settings #{reason}")
|
|
277
281
|
true
|
|
278
282
|
else
|
|
@@ -294,7 +298,7 @@ module Aikido::Zen
|
|
|
294
298
|
begin
|
|
295
299
|
return false unless Aikido::Zen.api_cache.update_runtime_firewall_lists(data)
|
|
296
300
|
|
|
297
|
-
if Aikido::Zen.
|
|
301
|
+
if Aikido::Zen.firewall.update_from_json(data)
|
|
298
302
|
@config.logger.info("Updated runtime firewall list #{reason}")
|
|
299
303
|
true
|
|
300
304
|
else
|
data/lib/aikido/zen/config.rb
CHANGED
|
@@ -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
|
|
@@ -245,6 +249,7 @@ module Aikido::Zen
|
|
|
245
249
|
self.api_timeouts = 10
|
|
246
250
|
self.polling_interval = 60 # 1 min
|
|
247
251
|
self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
|
|
252
|
+
self.agent_mode = ENV.fetch("AIKIDO_AGENT_MODE", "shared")
|
|
248
253
|
self.worker_process_polling_interval = 10
|
|
249
254
|
self.worker_process_polling_jitter = 10
|
|
250
255
|
self.worker_process_heartbeat_interval = 10
|
|
@@ -299,6 +304,17 @@ module Aikido::Zen
|
|
|
299
304
|
@realtime_endpoint = URI(url)
|
|
300
305
|
end
|
|
301
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
|
+
|
|
302
318
|
# Set the logger and configure its severity level according to agent's debug mode
|
|
303
319
|
# @param logger [::Logger]
|
|
304
320
|
def logger=(logger)
|
|
@@ -370,6 +386,9 @@ module Aikido::Zen
|
|
|
370
386
|
end
|
|
371
387
|
end
|
|
372
388
|
|
|
389
|
+
# @!visibility private
|
|
390
|
+
AGENT_MODES = [:shared, :per_worker].freeze
|
|
391
|
+
|
|
373
392
|
# @!visibility private
|
|
374
393
|
DEFAULT_AIKIDO_ENDPOINT = "https://guard.aikido.dev"
|
|
375
394
|
|
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)
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
module Aikido::Zen
|
|
4
4
|
module Middleware
|
|
5
5
|
class IPListChecker
|
|
6
|
-
def initialize(app, zen: Aikido::Zen, config: zen.config,
|
|
6
|
+
def initialize(app, zen: Aikido::Zen, config: zen.config, firewall: zen.firewall)
|
|
7
7
|
@app = app
|
|
8
8
|
@zen = zen
|
|
9
9
|
@config = config
|
|
10
|
-
@
|
|
10
|
+
@firewall = firewall
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def call(env)
|
|
@@ -17,14 +17,14 @@ module Aikido::Zen
|
|
|
17
17
|
|
|
18
18
|
return @app.call(env) if @zen.request_bypassed?
|
|
19
19
|
|
|
20
|
-
if !@
|
|
20
|
+
if !@firewall.allowed_ip?(client_ip)
|
|
21
21
|
return @config.blocked_responder.call(request, :ip_allowed_list)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
monitored_ip_list_keys = @
|
|
24
|
+
monitored_ip_list_keys = @firewall.monitored_ip_list_keys(client_ip)
|
|
25
25
|
@zen.track_ip_list(monitored_ip_list_keys)
|
|
26
26
|
|
|
27
|
-
blocked_ip_lists = @
|
|
27
|
+
blocked_ip_lists = @firewall.blocked_ip_lists.filter { |ip_list| ip_list.include?(client_ip) }
|
|
28
28
|
|
|
29
29
|
if !blocked_ip_lists.empty?
|
|
30
30
|
@zen.track_ip_list(blocked_ip_lists.map(&:key))
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
module Aikido::Zen
|
|
4
4
|
module Middleware
|
|
5
5
|
class UserAgentChecker
|
|
6
|
-
def initialize(app, zen: Aikido::Zen, config: zen.config,
|
|
6
|
+
def initialize(app, zen: Aikido::Zen, config: zen.config, firewall: zen.firewall)
|
|
7
7
|
@app = app
|
|
8
8
|
@zen = zen
|
|
9
9
|
@config = config
|
|
10
|
-
@
|
|
10
|
+
@firewall = firewall
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def call(env)
|
|
@@ -17,15 +17,15 @@ module Aikido::Zen
|
|
|
17
17
|
|
|
18
18
|
user_agent = request.user_agent
|
|
19
19
|
|
|
20
|
-
if @
|
|
21
|
-
user_agent_keys = @
|
|
20
|
+
if @firewall.blocked_user_agent?(user_agent)
|
|
21
|
+
user_agent_keys = @firewall.user_agent_keys(user_agent)
|
|
22
22
|
@zen.track_user_agent(user_agent_keys)
|
|
23
23
|
|
|
24
24
|
return @config.blocked_responder.call(request, :user_agent)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
if @
|
|
28
|
-
user_agent_keys = @
|
|
27
|
+
if @firewall.monitored_user_agent?(user_agent)
|
|
28
|
+
user_agent_keys = @firewall.user_agent_keys(user_agent)
|
|
29
29
|
@zen.track_user_agent(user_agent_keys)
|
|
30
30
|
end
|
|
31
31
|
|
|
@@ -3,16 +3,6 @@
|
|
|
3
3
|
require "set"
|
|
4
4
|
|
|
5
5
|
module Aikido::Zen
|
|
6
|
-
# Stores the firewall configuration sourced from the Aikido dashboard. This
|
|
7
|
-
# object is updated by the Agent regularly.
|
|
8
|
-
#
|
|
9
|
-
# Because the RuntimeSettings object can be modified in runtime, it implements
|
|
10
|
-
# the {Observable} API, allowing you to subscribe to updates. These are
|
|
11
|
-
# triggered whenever #update_from_runtime_settings_json makes a change
|
|
12
|
-
# (i.e. if the settings don't change, no update is triggered).
|
|
13
|
-
#
|
|
14
|
-
# You can subscribe to changes with +#add_observer(object, func_name)+, which
|
|
15
|
-
# will call the function passing the settings as an argument
|
|
16
6
|
RuntimeSettings = Struct.new(
|
|
17
7
|
:updated_at,
|
|
18
8
|
:heartbeat_interval,
|
|
@@ -21,12 +11,6 @@ module Aikido::Zen
|
|
|
21
11
|
:bypassed_ips,
|
|
22
12
|
:received_any_stats,
|
|
23
13
|
:blocking_mode,
|
|
24
|
-
:blocked_user_agent_regexp,
|
|
25
|
-
:monitored_user_agent_regexp,
|
|
26
|
-
:user_agent_details,
|
|
27
|
-
:blocked_ip_lists,
|
|
28
|
-
:allowed_ip_lists,
|
|
29
|
-
:monitored_ip_lists,
|
|
30
14
|
:block_new_outbound,
|
|
31
15
|
:domains,
|
|
32
16
|
:excluded_user_ids_from_rate_limiting,
|
|
@@ -36,9 +20,6 @@ module Aikido::Zen
|
|
|
36
20
|
super
|
|
37
21
|
self.endpoints ||= RuntimeSettings::Endpoints.new
|
|
38
22
|
self.bypassed_ips ||= RuntimeSettings::IPSet.new
|
|
39
|
-
self.blocked_ip_lists ||= []
|
|
40
|
-
self.allowed_ip_lists ||= []
|
|
41
|
-
self.monitored_ip_lists ||= []
|
|
42
23
|
self.domains ||= RuntimeSettings::Domains.new
|
|
43
24
|
self.enabled_features ||= Set.new
|
|
44
25
|
end
|
|
@@ -66,24 +47,6 @@ module Aikido::Zen
|
|
|
66
47
|
# @!attribute [rw] blocking_mode
|
|
67
48
|
# @return [Boolean]
|
|
68
49
|
|
|
69
|
-
# @!attribute [rw] blocked_ip_lists
|
|
70
|
-
# @return [Array<Aikido::Zen::RuntimeSettings::IPList>]
|
|
71
|
-
|
|
72
|
-
# @!attribute [rw] allowed_ip_lists
|
|
73
|
-
# @return [Array<Aikido::Zen::RuntimeSettings::IPList>]
|
|
74
|
-
|
|
75
|
-
# @!attribute [rw] monitored_ip_lists
|
|
76
|
-
# @return [Array<Aikido::Zen::RuntimeSettings::IPList>]
|
|
77
|
-
|
|
78
|
-
# @!attribute [rw] blocked_user_agent_regexp
|
|
79
|
-
# @return [Regexp]
|
|
80
|
-
|
|
81
|
-
# @!attribute [rw] monitored_user_agent_regexp
|
|
82
|
-
# @return [Regexp]
|
|
83
|
-
|
|
84
|
-
# @!attribute [rw] user_agent_details
|
|
85
|
-
# @return [Regexp]
|
|
86
|
-
|
|
87
50
|
# @!attribute [rw] block_new_outbound
|
|
88
51
|
# @return [Boolean]
|
|
89
52
|
|
|
@@ -97,15 +60,9 @@ module Aikido::Zen
|
|
|
97
60
|
# @!attribute [rw] enabled_features
|
|
98
61
|
# @return [Set<String>]
|
|
99
62
|
|
|
100
|
-
# Parse and interpret the JSON response from the core API with updated
|
|
101
|
-
# runtime settings, and apply the changes.
|
|
102
|
-
#
|
|
103
|
-
# This will also notify any subscriber to updates.
|
|
104
|
-
#
|
|
105
63
|
# @param data [Hash] the decoded JSON payload from the /api/runtime/config
|
|
106
|
-
# API endpoint.
|
|
107
64
|
# @return [Boolean]
|
|
108
|
-
def
|
|
65
|
+
def update_from_json(data)
|
|
109
66
|
last_updated_at = updated_at
|
|
110
67
|
|
|
111
68
|
self.updated_at = Time.at(data["configUpdatedAt"].to_i)
|
|
@@ -126,69 +83,6 @@ module Aikido::Zen
|
|
|
126
83
|
updated_at != last_updated_at
|
|
127
84
|
end
|
|
128
85
|
|
|
129
|
-
# Parse and interpret the JSON response from the core API with updated
|
|
130
|
-
# runtime firewall lists, and apply the changes.
|
|
131
|
-
#
|
|
132
|
-
# @param data [Hash] the decoded JSON payload from the /api/runtime/firewall/lists
|
|
133
|
-
# API endpoint.
|
|
134
|
-
# @return [Boolean]
|
|
135
|
-
def update_from_runtime_firewall_lists_json(data)
|
|
136
|
-
self.blocked_user_agent_regexp = pattern(data["blockedUserAgents"])
|
|
137
|
-
|
|
138
|
-
self.monitored_user_agent_regexp = pattern(data["monitoredUserAgents"])
|
|
139
|
-
|
|
140
|
-
self.user_agent_details = []
|
|
141
|
-
|
|
142
|
-
data["userAgentDetails"]&.each do |record|
|
|
143
|
-
key = record["key"]
|
|
144
|
-
pattern = pattern(record["pattern"])
|
|
145
|
-
|
|
146
|
-
next if key.nil? || pattern.nil?
|
|
147
|
-
|
|
148
|
-
user_agent_details << {
|
|
149
|
-
key: key,
|
|
150
|
-
pattern: pattern
|
|
151
|
-
}
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
self.blocked_ip_lists = []
|
|
155
|
-
|
|
156
|
-
data["blockedIPAddresses"]&.each do |ip_list|
|
|
157
|
-
blocked_ip_lists << RuntimeSettings::IPList.from_json(ip_list)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
self.allowed_ip_lists = []
|
|
161
|
-
|
|
162
|
-
data["allowedIPAddresses"]&.each do |ip_list|
|
|
163
|
-
allowed_ip_lists << RuntimeSettings::IPList.from_json(ip_list)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
self.monitored_ip_lists = []
|
|
167
|
-
|
|
168
|
-
data["monitoredIPAddresses"]&.each do |ip_list|
|
|
169
|
-
monitored_ip_lists << RuntimeSettings::IPList.from_json(ip_list)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
true
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Construct a regular expression from the non-nil and non-empty string,
|
|
176
|
-
# otherwise return nil.
|
|
177
|
-
#
|
|
178
|
-
# The resulting regular expression is case insensitive.
|
|
179
|
-
#
|
|
180
|
-
# @param string [String, nil]
|
|
181
|
-
# @return [Regexp, nil]
|
|
182
|
-
private def pattern(string)
|
|
183
|
-
return nil if string.nil? || string.empty?
|
|
184
|
-
|
|
185
|
-
begin
|
|
186
|
-
/#{string}/i
|
|
187
|
-
rescue RegexpError
|
|
188
|
-
nil
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
86
|
# @param ip [String]
|
|
193
87
|
# @return [Boolean] Whether the IP is included in the bypassed IPs set.
|
|
194
88
|
def bypassed_ip?(ip)
|
|
@@ -202,48 +96,6 @@ module Aikido::Zen
|
|
|
202
96
|
excluded_user_ids_from_rate_limiting&.include?(user_id.to_s) || false
|
|
203
97
|
end
|
|
204
98
|
|
|
205
|
-
# @param user_agent [String] the user agent
|
|
206
|
-
# @return [Boolean] whether the user agent should be blocked
|
|
207
|
-
def blocked_user_agent?(user_agent)
|
|
208
|
-
return false if blocked_user_agent_regexp.nil?
|
|
209
|
-
|
|
210
|
-
blocked_user_agent_regexp.match?(user_agent)
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# @param user_agent [String] the user agent
|
|
214
|
-
# @return [Boolean] whether the user agent should be monitored
|
|
215
|
-
def monitored_user_agent?(user_agent)
|
|
216
|
-
return false if monitored_user_agent_regexp.nil?
|
|
217
|
-
|
|
218
|
-
monitored_user_agent_regexp.match?(user_agent)
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# @param user_agent [String] the user agent
|
|
222
|
-
# @return [Array<String>] the matching user agent keys
|
|
223
|
-
def user_agent_keys(user_agent)
|
|
224
|
-
return [] if user_agent_details.nil?
|
|
225
|
-
|
|
226
|
-
user_agent_details.filter_map { |record| record[:key] if record[:pattern].match?(user_agent) }
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def allowed_ip?(ip)
|
|
230
|
-
allowed_ip_lists.empty? || allowed_ip_lists.any? { |ip_list| ip_list.include?(ip) }
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def blocked_ip?(ip)
|
|
234
|
-
blocked_ip_lists.any? { |ip_list| ip_list.include?(ip) }
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def monitored_ip?(ip)
|
|
238
|
-
monitored_ip_lists.any? { |ip_list| ip_list.include?(ip) }
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def monitored_ip_list_keys(ip)
|
|
242
|
-
return [] if ip.nil?
|
|
243
|
-
|
|
244
|
-
monitored_ip_lists.filter_map { |ip_list| ip_list.key if ip_list.include?(ip) }
|
|
245
|
-
end
|
|
246
|
-
|
|
247
99
|
def block_outbound?(connection)
|
|
248
100
|
domain = domains[connection.host]
|
|
249
101
|
|
|
@@ -261,6 +113,5 @@ module Aikido::Zen
|
|
|
261
113
|
end
|
|
262
114
|
|
|
263
115
|
require_relative "runtime_settings/ip_set"
|
|
264
|
-
require_relative "runtime_settings/ip_list"
|
|
265
116
|
require_relative "runtime_settings/endpoints"
|
|
266
117
|
require_relative "runtime_settings/domains"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "resolv"
|
|
4
4
|
require "ipaddr"
|
|
5
|
+
require "socket"
|
|
5
6
|
|
|
6
7
|
module Aikido::Zen
|
|
7
8
|
module Scanners
|
|
@@ -72,17 +73,51 @@ module Aikido::Zen
|
|
|
72
73
|
|
|
73
74
|
PRIVATE_RANGES = PRIVATE_IPV4_RANGES + PRIVATE_IPV6_RANGES + PRIVATE_IPV4_RANGES.map(&:ipv4_mapped)
|
|
74
75
|
|
|
76
|
+
# DNS lookups already performed for this request, keyed by hostname.
|
|
77
|
+
#
|
|
78
|
+
# @return [Aikido::Zen::Scanners::SSRF::DNSLookups, nil] nil outside
|
|
79
|
+
# of a request
|
|
75
80
|
def resolved_in_current_context
|
|
76
81
|
context = Aikido::Zen.current_context
|
|
77
82
|
context && context["dns.lookups"]
|
|
78
83
|
end
|
|
79
84
|
|
|
85
|
+
# Matches strings that contain only characters that appear in some
|
|
86
|
+
# form of IP address.
|
|
87
|
+
#
|
|
88
|
+
# Hostnames are not expected to match, allowing `parse_address` to
|
|
89
|
+
# skip the `Socket.getaddrinfo` call.
|
|
90
|
+
ADDRESS_REGEXP = /\A[0-9a-fx:.]+\z/i
|
|
91
|
+
|
|
92
|
+
# Parses `address` as an IP address, in any form that is accepted
|
|
93
|
+
# by `getaddrinfo`:
|
|
94
|
+
#
|
|
95
|
+
# * Standard IPv4/IPv6 notation (e.g. "127.0.0.1", "::1")
|
|
96
|
+
# * Plain integer IPv4 notation, in decimal, octal, or hexadecimal
|
|
97
|
+
# (e.g. "2130706433", "017700000001", "0x7f000001")
|
|
98
|
+
# * Shorthand dotted IPv4 notation (e.g. "127.1")
|
|
99
|
+
#
|
|
100
|
+
# Delegates to `Socket.getaddrinfo` with the `AI_NUMERICHOST` flag,
|
|
101
|
+
# which never performs a DNS lookup.
|
|
102
|
+
#
|
|
103
|
+
# @param address [String]
|
|
104
|
+
# @return [Array<IPAddr>, nil] nil if `address` is not an address
|
|
105
|
+
def parse_address(address)
|
|
106
|
+
return nil unless address.is_a?(String) && ADDRESS_REGEXP.match?(address)
|
|
107
|
+
|
|
108
|
+
Socket.getaddrinfo(address, nil, :UNSPEC, :STREAM, nil, Socket::AI_NUMERICHOST)
|
|
109
|
+
.map { |info| IPAddr.new(info[3]) }
|
|
110
|
+
rescue SocketError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
80
114
|
def resolve(hostname_or_address)
|
|
81
115
|
return [] if hostname_or_address.nil?
|
|
82
116
|
|
|
117
|
+
addresses = parse_address(hostname_or_address)
|
|
118
|
+
return addresses if addresses
|
|
119
|
+
|
|
83
120
|
case hostname_or_address
|
|
84
|
-
when Resolv::AddressRegex
|
|
85
|
-
[IPAddr.new(hostname_or_address)]
|
|
86
121
|
when resolved_in_current_context
|
|
87
122
|
resolved_in_current_context[hostname_or_address]
|
|
88
123
|
.map { |address| IPAddr.new(address) }
|
|
@@ -6,9 +6,9 @@ require_relative "../scanners/ssrf_scanner"
|
|
|
6
6
|
|
|
7
7
|
module Aikido::Zen
|
|
8
8
|
module Sinks
|
|
9
|
-
# We intercept
|
|
10
|
-
# there's no way to access the internal DNS resolution that
|
|
11
|
-
# when using the socket primitives.
|
|
9
|
+
# We intercept TCPSocket#initialize to hook our DNS checks around it,
|
|
10
|
+
# since there's no way to access the internal DNS resolution that
|
|
11
|
+
# happens in C when using the socket primitives.
|
|
12
12
|
module Socket
|
|
13
13
|
SINK = Sinks.add("socket", "outgoing_http_op", scanners: [
|
|
14
14
|
Scanners::StoredSSRFScanner,
|
|
@@ -17,7 +17,7 @@ module Aikido::Zen
|
|
|
17
17
|
|
|
18
18
|
module Helpers
|
|
19
19
|
def self.scan(hostname, socket, operation)
|
|
20
|
-
# We're patching
|
|
20
|
+
# We're patching TCPSocket#initialize.
|
|
21
21
|
# The IPSocket class hierarchy is:
|
|
22
22
|
# IPSocket
|
|
23
23
|
# / \
|
|
@@ -59,20 +59,22 @@ module Aikido::Zen
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def self.load_sinks!
|
|
62
|
-
::
|
|
62
|
+
::TCPSocket.class_eval do
|
|
63
63
|
extend Sinks::DSL
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
# This sink method covers both the `open` and the `new` class methods,
|
|
66
|
+
# since these methods call `initialize`.
|
|
67
|
+
sink_after :initialize do |_result, remote_host, *|
|
|
66
68
|
# Code coverage is disabled here because the tests are contrived and
|
|
67
69
|
# intentionally do not call open.
|
|
68
70
|
# :nocov:
|
|
69
|
-
Helpers.scan(remote_host,
|
|
71
|
+
Helpers.scan(remote_host, self, "open")
|
|
70
72
|
# :nocov:
|
|
71
73
|
rescue Aikido::Zen::UnderAttackError, Aikido::Zen::Sinks::DSL::PresafeError
|
|
72
74
|
# If the scan raises an exception that will escape the safe block,
|
|
73
75
|
# the open socket must be closed because it will not be returned,
|
|
74
76
|
# so the user cannot close it.
|
|
75
|
-
|
|
77
|
+
close
|
|
76
78
|
|
|
77
79
|
raise
|
|
78
80
|
end
|
data/lib/aikido/zen/version.rb
CHANGED
|
@@ -107,14 +107,14 @@ module Aikido::Zen::WorkerProcess
|
|
|
107
107
|
|
|
108
108
|
if settings["config"]
|
|
109
109
|
@config.logger.debug("Forked worker process #{Process.pid}: starting config update")
|
|
110
|
-
Aikido::Zen.runtime_settings.
|
|
110
|
+
Aikido::Zen.runtime_settings.update_from_json(settings["config"])
|
|
111
111
|
@known_config_generation = settings["config_generation"]
|
|
112
112
|
@config.logger.debug("Forked worker process #{Process.pid}: finished config update")
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
if settings["firewall_lists"]
|
|
116
116
|
@config.logger.debug("Forked worker process #{Process.pid}: starting firewall_lists update")
|
|
117
|
-
Aikido::Zen.
|
|
117
|
+
Aikido::Zen.firewall.update_from_json(settings["firewall_lists"])
|
|
118
118
|
@known_firewall_lists_generation = settings["firewall_lists_generation"]
|
|
119
119
|
@config.logger.debug("Forked worker process #{Process.pid}: finished firewall_lists update")
|
|
120
120
|
end
|
data/lib/aikido/zen.rb
CHANGED
|
@@ -27,6 +27,7 @@ require_relative "zen/middleware/attack_wave_protector"
|
|
|
27
27
|
require_relative "zen/middleware/request_tracker"
|
|
28
28
|
require_relative "zen/outbound_connection"
|
|
29
29
|
require_relative "zen/runtime_settings"
|
|
30
|
+
require_relative "zen/firewall"
|
|
30
31
|
require_relative "zen/rate_limiter"
|
|
31
32
|
require_relative "zen/attack_wave"
|
|
32
33
|
require_relative "zen/sql"
|
|
@@ -88,6 +89,14 @@ module Aikido
|
|
|
88
89
|
@runtime_settings = settings
|
|
89
90
|
end
|
|
90
91
|
|
|
92
|
+
def self.firewall
|
|
93
|
+
@firewall ||= Firewall.new
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.firewall=(firewall)
|
|
97
|
+
@firewall = firewall
|
|
98
|
+
end
|
|
99
|
+
|
|
91
100
|
def self.api_cache
|
|
92
101
|
@api_cache ||= APICache.new
|
|
93
102
|
end
|
|
@@ -269,6 +278,35 @@ module Aikido
|
|
|
269
278
|
alias_method :set_user, :track_user
|
|
270
279
|
end
|
|
271
280
|
|
|
281
|
+
# Track a custom event happening in your application, like a failed login or
|
|
282
|
+
# a password reset request. Zen automatically attaches the IP address, user
|
|
283
|
+
# agent, and current user (if you called .track_user) from the request.
|
|
284
|
+
#
|
|
285
|
+
# Only works inside an HTTP request; if called from a background job or
|
|
286
|
+
# script, nothing gets sent.
|
|
287
|
+
#
|
|
288
|
+
# @param name [String]
|
|
289
|
+
# @return [void]
|
|
290
|
+
def self.track_custom_event(name)
|
|
291
|
+
unless name.is_a?(String) && !name.empty?
|
|
292
|
+
config.logger.warn("track_custom_event expects a non-empty String as the event name")
|
|
293
|
+
return
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
return unless agent
|
|
297
|
+
|
|
298
|
+
context = current_context
|
|
299
|
+
return unless context
|
|
300
|
+
|
|
301
|
+
event = Events::Custom.new(
|
|
302
|
+
name: name,
|
|
303
|
+
request: context.request,
|
|
304
|
+
user: context.request.actor
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
agent.report_custom_event(event)
|
|
308
|
+
end
|
|
309
|
+
|
|
272
310
|
# @return [Aikido::Zen::AttackWave::Detector] the attack wave detector.
|
|
273
311
|
def self.attack_wave_detector
|
|
274
312
|
@attack_wave_detector ||= AttackWave::Detector.new
|
|
@@ -411,10 +449,19 @@ module Aikido
|
|
|
411
449
|
# @!visibility private
|
|
412
450
|
# Stop any background threads.
|
|
413
451
|
def self.stop!
|
|
414
|
-
@agent
|
|
415
|
-
@
|
|
452
|
+
agent = @agent
|
|
453
|
+
@agent = nil
|
|
454
|
+
agent&.stop!
|
|
455
|
+
|
|
456
|
+
server = @worker_process_server
|
|
457
|
+
@worker_process_server = nil
|
|
458
|
+
server&.stop
|
|
416
459
|
|
|
417
|
-
@worker_process_client
|
|
460
|
+
client = @worker_process_client
|
|
461
|
+
@worker_process_client = nil
|
|
462
|
+
client&.stop
|
|
463
|
+
|
|
464
|
+
@running.make_false
|
|
418
465
|
end
|
|
419
466
|
|
|
420
467
|
def self.agent
|
|
@@ -425,13 +472,13 @@ module Aikido
|
|
|
425
472
|
@worker_process_server
|
|
426
473
|
end
|
|
427
474
|
|
|
428
|
-
@
|
|
475
|
+
@running = Concurrent::AtomicBoolean.new(false)
|
|
429
476
|
|
|
430
477
|
class << self
|
|
431
478
|
def start!
|
|
432
479
|
return unless start?
|
|
433
480
|
|
|
434
|
-
return unless @
|
|
481
|
+
return unless @running.make_true
|
|
435
482
|
|
|
436
483
|
@worker_process_server = WorkerProcess::Agent::Server.new
|
|
437
484
|
@worker_process_server.start
|
|
@@ -446,9 +493,9 @@ module Aikido
|
|
|
446
493
|
end
|
|
447
494
|
|
|
448
495
|
def fork!
|
|
449
|
-
|
|
450
|
-
return unless server
|
|
496
|
+
return unless @running.true?
|
|
451
497
|
|
|
498
|
+
server = @worker_process_server
|
|
452
499
|
@worker_process_server = nil
|
|
453
500
|
server.close
|
|
454
501
|
|
|
@@ -456,11 +503,30 @@ module Aikido
|
|
|
456
503
|
@worker_process_client = nil
|
|
457
504
|
client&.close
|
|
458
505
|
|
|
506
|
+
case config.agent_mode
|
|
507
|
+
when :per_worker
|
|
508
|
+
start_per_worker_mode
|
|
509
|
+
when :shared
|
|
510
|
+
start_shared_mode(server)
|
|
511
|
+
end
|
|
512
|
+
rescue => err
|
|
513
|
+
config.logger.error("Forked worker process #{Process.pid}: failed to start: #{err.message}")
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
private
|
|
517
|
+
|
|
518
|
+
def start_per_worker_mode
|
|
519
|
+
agent = @agent
|
|
520
|
+
@agent = nil
|
|
521
|
+
agent&.stop!
|
|
522
|
+
|
|
523
|
+
@agent = Agent.start
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def start_shared_mode(server)
|
|
459
527
|
client = WorkerProcess::Agent::Client.new(server.host, server.port)
|
|
460
528
|
client.start
|
|
461
529
|
@worker_process_client = client
|
|
462
|
-
rescue => err
|
|
463
|
-
config.logger.error("Forked worker process #{Process.pid}: failed to start worker process client: #{err.message}")
|
|
464
530
|
end
|
|
465
531
|
end
|
|
466
532
|
|
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.
|
|
4
|
+
version: 1.9.0.beta.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-
|
|
11
|
+
date: 2026-09-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -89,6 +89,7 @@ files:
|
|
|
89
89
|
- benchmarks/rpc/benchmark.rb
|
|
90
90
|
- docs/banner.svg
|
|
91
91
|
- docs/config.md
|
|
92
|
+
- docs/custom-event-tracking.md
|
|
92
93
|
- docs/idor-protection.md
|
|
93
94
|
- docs/invalid-sql-queries.md
|
|
94
95
|
- docs/proxy.md
|
|
@@ -123,6 +124,8 @@ files:
|
|
|
123
124
|
- lib/aikido/zen/current_context.rb
|
|
124
125
|
- lib/aikido/zen/errors.rb
|
|
125
126
|
- lib/aikido/zen/event.rb
|
|
127
|
+
- lib/aikido/zen/firewall.rb
|
|
128
|
+
- lib/aikido/zen/firewall/ip_list.rb
|
|
126
129
|
- lib/aikido/zen/helpers.rb
|
|
127
130
|
- lib/aikido/zen/idor.rb
|
|
128
131
|
- lib/aikido/zen/idor/analysis_result.rb
|
|
@@ -163,7 +166,6 @@ files:
|
|
|
163
166
|
- lib/aikido/zen/runtime_settings/domain_settings.rb
|
|
164
167
|
- lib/aikido/zen/runtime_settings/domains.rb
|
|
165
168
|
- lib/aikido/zen/runtime_settings/endpoints.rb
|
|
166
|
-
- lib/aikido/zen/runtime_settings/ip_list.rb
|
|
167
169
|
- lib/aikido/zen/runtime_settings/ip_set.rb
|
|
168
170
|
- lib/aikido/zen/runtime_settings/protection_settings.rb
|
|
169
171
|
- lib/aikido/zen/runtime_settings/rate_limit_settings.rb
|