aikido-zen 1.0.1.beta.5-arm64-darwin → 1.0.2-arm64-darwin
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/.simplecov +6 -0
- data/README.md +2 -0
- data/benchmarks/README.md +0 -1
- data/benchmarks/rails7.1_benchmark.js +1 -0
- data/benchmarks/rails7.1_sql_injection.js +52 -20
- data/docs/config.md +9 -1
- data/docs/proxy.md +10 -0
- data/docs/rails.md +55 -13
- data/docs/troubleshooting.md +62 -0
- data/lib/aikido/zen/actor.rb +34 -4
- data/lib/aikido/zen/agent/heartbeats_manager.rb +5 -5
- data/lib/aikido/zen/agent.rb +19 -17
- data/lib/aikido/zen/attack.rb +19 -9
- data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
- data/lib/aikido/zen/attack_wave.rb +88 -0
- data/lib/aikido/zen/cache.rb +91 -0
- data/lib/aikido/zen/capped_collections.rb +22 -4
- data/lib/aikido/zen/collector/event.rb +238 -0
- data/lib/aikido/zen/collector/hosts.rb +16 -1
- data/lib/aikido/zen/collector/routes.rb +13 -8
- data/lib/aikido/zen/collector/stats.rb +33 -22
- data/lib/aikido/zen/collector/users.rb +5 -3
- data/lib/aikido/zen/collector.rb +107 -28
- data/lib/aikido/zen/config.rb +54 -21
- data/lib/aikido/zen/context/rack_request.rb +3 -0
- data/lib/aikido/zen/context/rails_request.rb +3 -0
- data/lib/aikido/zen/context.rb +42 -9
- data/lib/aikido/zen/detached_agent/agent.rb +28 -27
- data/lib/aikido/zen/detached_agent/front_object.rb +10 -6
- data/lib/aikido/zen/detached_agent/server.rb +63 -26
- data/lib/aikido/zen/event.rb +47 -2
- data/lib/aikido/zen/helpers.rb +24 -0
- data/lib/aikido/zen/internals.rb +23 -3
- data/lib/aikido/zen/libzen-v0.1.48-arm64-darwin.dylib +0 -0
- data/lib/aikido/zen/middleware/{check_allowed_addresses.rb → allowed_address_checker.rb} +1 -1
- data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
- data/lib/aikido/zen/middleware/{set_context.rb → context_setter.rb} +1 -1
- data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
- data/lib/aikido/zen/middleware/rack_throttler.rb +3 -1
- data/lib/aikido/zen/middleware/request_tracker.rb +9 -4
- data/lib/aikido/zen/outbound_connection.rb +18 -1
- data/lib/aikido/zen/payload.rb +1 -1
- data/lib/aikido/zen/rails_engine.rb +5 -8
- data/lib/aikido/zen/request/rails_router.rb +17 -2
- data/lib/aikido/zen/request.rb +21 -36
- data/lib/aikido/zen/route.rb +57 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +37 -8
- data/lib/aikido/zen/runtime_settings.rb +6 -5
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +10 -7
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +5 -4
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +3 -2
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +3 -2
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +2 -1
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +8 -2
- data/lib/aikido/zen/sink.rb +1 -1
- data/lib/aikido/zen/sinks/action_controller.rb +3 -1
- data/lib/aikido/zen/sinks/file.rb +45 -4
- data/lib/aikido/zen/sinks/kernel.rb +1 -1
- data/lib/aikido/zen/sinks/socket.rb +7 -0
- data/lib/aikido/zen/sinks_dsl.rb +12 -0
- data/lib/aikido/zen/system_info.rb +1 -5
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen.rb +77 -15
- data/tasklib/bench.rake +1 -1
- data/tasklib/libzen.rake +1 -0
- metadata +15 -5
- data/lib/aikido/zen/libzen-v0.1.39-arm64-darwin.dylib +0 -0
data/lib/aikido/zen/config.rb
CHANGED
|
@@ -8,16 +8,10 @@ require_relative "context"
|
|
|
8
8
|
|
|
9
9
|
module Aikido::Zen
|
|
10
10
|
class Config
|
|
11
|
-
# @api private
|
|
12
|
-
# @return [Boolean] whether Aikido should protect.
|
|
13
|
-
def protect?
|
|
14
|
-
!api_token.nil? || blocking_mode? || debugging?
|
|
15
|
-
end
|
|
16
|
-
|
|
17
11
|
# @return [Boolean] whether Aikido should be turned completely off (no
|
|
18
12
|
# intercepting calls to protect the app, no agent process running, no
|
|
19
13
|
# middleware installed). Defaults to false (so, enabled). Can be set
|
|
20
|
-
# via the
|
|
14
|
+
# via the AIKIDO_DISABLE environment variable.
|
|
21
15
|
attr_accessor :disabled
|
|
22
16
|
alias_method :disabled?, :disabled
|
|
23
17
|
|
|
@@ -46,9 +40,9 @@ module Aikido::Zen
|
|
|
46
40
|
# settings changes. Defaults to evey 60 seconds.
|
|
47
41
|
attr_accessor :polling_interval
|
|
48
42
|
|
|
49
|
-
# @return [Integer] the
|
|
50
|
-
# heartbeat event
|
|
51
|
-
attr_accessor :
|
|
43
|
+
# @return [Array<Integer>] the delays in seconds to wait before sending
|
|
44
|
+
# each initial heartbeat event.
|
|
45
|
+
attr_accessor :initial_heartbeat_delays
|
|
52
46
|
|
|
53
47
|
# @return [#call] Callable that can be passed an Object and returns a String
|
|
54
48
|
# of JSON. Defaults to the standard library's JSON.dump method.
|
|
@@ -61,15 +55,18 @@ module Aikido::Zen
|
|
|
61
55
|
# @return [Logger]
|
|
62
56
|
attr_reader :logger
|
|
63
57
|
|
|
64
|
-
# @return [
|
|
58
|
+
# @return [String] Path of the socket where the detached agent will listen.
|
|
65
59
|
# By default, is stored under the root application path with file name
|
|
66
60
|
# `aikido-detached-agent.sock`
|
|
67
|
-
|
|
61
|
+
attr_accessor :detached_agent_socket_path
|
|
68
62
|
|
|
69
63
|
# @return [Boolean] is the agent in debugging mode?
|
|
70
64
|
attr_accessor :debugging
|
|
71
65
|
alias_method :debugging?, :debugging
|
|
72
66
|
|
|
67
|
+
# @return [String] environment specific HTTP header providing the client IP.
|
|
68
|
+
attr_accessor :client_ip_header
|
|
69
|
+
|
|
73
70
|
# @return [Integer] maximum number of timing measurements to keep in memory
|
|
74
71
|
# before compressing them.
|
|
75
72
|
attr_accessor :max_performance_samples
|
|
@@ -146,25 +143,56 @@ module Aikido::Zen
|
|
|
146
143
|
# the server returns a 429 response.
|
|
147
144
|
attr_accessor :server_rate_limit_deadline
|
|
148
145
|
|
|
146
|
+
# @return [Boolean] whether Aikido Zen should scan for stored SSSRF attacks.
|
|
147
|
+
# Defaults to true. Can be set through AIKIDO_FEATURE_STORED_SSRF
|
|
148
|
+
# environment variable.
|
|
149
|
+
attr_accessor :stored_ssrf
|
|
150
|
+
alias_method :stored_ssrf?, :stored_ssrf
|
|
151
|
+
|
|
149
152
|
# @return [Array<String>] when checking for stored SSRF attacks, we want to
|
|
150
153
|
# allow known hosts that should be able to resolve to the IMDS service.
|
|
151
154
|
attr_accessor :imds_allowed_hosts
|
|
152
155
|
|
|
156
|
+
# @return [Boolean] whether Aikido Zen should harden methods where possible.
|
|
157
|
+
# Defaults to true. Can be set through AIKIDO_HARDEN environment variable.
|
|
158
|
+
attr_accessor :harden
|
|
159
|
+
alias_method :harden?, :harden
|
|
160
|
+
|
|
161
|
+
# @return [Integer] how many suspicious requests are allowed before an
|
|
162
|
+
# attack wave detected event is reported.
|
|
163
|
+
# Defaults to 15 requests.
|
|
164
|
+
attr_accessor :attack_wave_threshold
|
|
165
|
+
|
|
166
|
+
# @return [Integer] the minimum time in milliseconds between requests for
|
|
167
|
+
# requests to be part of an attack wave.
|
|
168
|
+
# Defaults to 1 minute in milliseconds.
|
|
169
|
+
attr_accessor :attack_wave_min_time_between_requests
|
|
170
|
+
|
|
171
|
+
# @return [Integer] the minimum time in milliseconds between reporting
|
|
172
|
+
# attack wave events.
|
|
173
|
+
# Defaults to 20 minutes in milliseconds.
|
|
174
|
+
attr_accessor :attack_wave_min_time_between_events
|
|
175
|
+
|
|
176
|
+
# @return [Integer] the maximum number of entries in the LRU cache.
|
|
177
|
+
# Defaults to 10,000 entries.
|
|
178
|
+
attr_accessor :attack_wave_max_cache_entries
|
|
179
|
+
|
|
153
180
|
def initialize
|
|
154
|
-
self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
|
|
181
|
+
self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
|
|
155
182
|
self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
|
|
156
183
|
self.api_timeouts = 10
|
|
157
184
|
self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT", DEFAULT_AIKIDO_ENDPOINT)
|
|
158
185
|
self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
|
|
159
186
|
self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
|
|
160
|
-
self.polling_interval = 60
|
|
161
|
-
self.
|
|
187
|
+
self.polling_interval = 60 # 1 min
|
|
188
|
+
self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
|
|
162
189
|
self.json_encoder = DEFAULT_JSON_ENCODER
|
|
163
190
|
self.json_decoder = DEFAULT_JSON_DECODER
|
|
164
191
|
self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
|
|
165
192
|
self.logger = Logger.new($stdout, progname: "aikido", level: debugging ? Logger::DEBUG : Logger::INFO)
|
|
166
|
-
self.max_performance_samples = 5000
|
|
167
193
|
self.detached_agent_socket_path = ENV.fetch("AIKIDO_DETACHED_AGENT_SOCKET_PATH", DEFAULT_DETACHED_AGENT_SOCKET_PATH)
|
|
194
|
+
self.client_ip_header = ENV.fetch("AIKIDO_CLIENT_IP_HEADER", nil)
|
|
195
|
+
self.max_performance_samples = 5000
|
|
168
196
|
self.max_compressed_stats = 100
|
|
169
197
|
self.max_outbound_connections = 200
|
|
170
198
|
self.max_users_tracked = 1000
|
|
@@ -172,14 +200,20 @@ module Aikido::Zen
|
|
|
172
200
|
self.blocked_responder = DEFAULT_BLOCKED_RESPONDER
|
|
173
201
|
self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
|
|
174
202
|
self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
|
|
175
|
-
self.server_rate_limit_deadline =
|
|
176
|
-
self.client_rate_limit_period =
|
|
203
|
+
self.server_rate_limit_deadline = 30 * 60 # 30 min
|
|
204
|
+
self.client_rate_limit_period = 60 * 60 # 1 hour
|
|
177
205
|
self.client_rate_limit_max_events = 100
|
|
178
206
|
self.collect_api_schema = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", true))
|
|
179
207
|
self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
|
|
180
208
|
self.api_schema_collection_max_depth = 20
|
|
181
209
|
self.api_schema_collection_max_properties = 20
|
|
210
|
+
self.stored_ssrf = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_STORED_SSRF", true))
|
|
182
211
|
self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
|
|
212
|
+
self.harden = read_boolean_from_env(ENV.fetch("AIKIDO_HARDEN", true))
|
|
213
|
+
self.attack_wave_threshold = 15
|
|
214
|
+
self.attack_wave_min_time_between_requests = 60 * 1000 # 1 min (ms)
|
|
215
|
+
self.attack_wave_min_time_between_events = 20 * 60 * 1000 # 20 min (ms)
|
|
216
|
+
self.attack_wave_max_cache_entries = 10_000
|
|
183
217
|
end
|
|
184
218
|
|
|
185
219
|
# Set the base URL for API requests.
|
|
@@ -222,9 +256,8 @@ module Aikido::Zen
|
|
|
222
256
|
@api_timeouts.update(value)
|
|
223
257
|
end
|
|
224
258
|
|
|
225
|
-
def
|
|
226
|
-
@detached_agent_socket_path
|
|
227
|
-
@detached_agent_socket_path = "drbunix:" + @detached_agent_socket_path unless @detached_agent_socket_path.start_with?("drbunix:")
|
|
259
|
+
def detached_agent_socket_uri
|
|
260
|
+
"drbunix:" + @detached_agent_socket_path
|
|
228
261
|
end
|
|
229
262
|
|
|
230
263
|
private
|
|
@@ -6,6 +6,9 @@ require_relative "../request/heuristic_router"
|
|
|
6
6
|
module Aikido::Zen
|
|
7
7
|
# @!visibility private
|
|
8
8
|
Context::RACK_REQUEST_BUILDER = ->(env) do
|
|
9
|
+
# Normalize PATH_INFO so routes are correctly recognized in middleware.
|
|
10
|
+
env["PATH_INFO"] = Helpers.normalize_path(env["PATH_INFO"])
|
|
11
|
+
|
|
9
12
|
delegate = Rack::Request.new(env)
|
|
10
13
|
router = Aikido::Zen::Request::HeuristicRouter.new
|
|
11
14
|
request = Aikido::Zen::Request.new(delegate, framework: "rack", router: router)
|
|
@@ -12,6 +12,9 @@ module Aikido::Zen
|
|
|
12
12
|
|
|
13
13
|
# @!visibility private
|
|
14
14
|
Context::RAILS_REQUEST_BUILDER = ->(env) do
|
|
15
|
+
# Normalize PATH_INFO so routes are correctly recognized in middleware.
|
|
16
|
+
env["PATH_INFO"] = Helpers.normalize_path(env["PATH_INFO"])
|
|
17
|
+
|
|
15
18
|
# Duplicate the Rack environment to prevent unexpected modifications from
|
|
16
19
|
# breaking Rails routing.
|
|
17
20
|
delegate = ActionDispatch::Request.new(env.dup)
|
data/lib/aikido/zen/context.rb
CHANGED
|
@@ -81,8 +81,8 @@ module Aikido::Zen
|
|
|
81
81
|
def protection_disabled?
|
|
82
82
|
return false if request.nil?
|
|
83
83
|
|
|
84
|
-
!@settings.endpoints
|
|
85
|
-
@settings.
|
|
84
|
+
!@settings.endpoints.match(request.route).all?(&:protected?) ||
|
|
85
|
+
@settings.allowed_ips.include?(request.ip)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
# @!visibility private
|
|
@@ -92,18 +92,51 @@ module Aikido::Zen
|
|
|
92
92
|
|
|
93
93
|
private
|
|
94
94
|
|
|
95
|
+
# @!visibility private
|
|
95
96
|
def extract_payloads_from(data, source_type, prefix = nil)
|
|
96
97
|
if data.respond_to?(:to_hash)
|
|
97
|
-
data.to_hash.flat_map
|
|
98
|
-
extract_payloads_from(
|
|
99
|
-
|
|
98
|
+
data.to_hash.flat_map do |key, value|
|
|
99
|
+
extract_payloads_from(value, source_type, [prefix, key].compact.join("."))
|
|
100
|
+
end
|
|
100
101
|
elsif data.respond_to?(:to_ary)
|
|
101
|
-
data.to_ary
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
array = data.to_ary
|
|
103
|
+
return array if array.empty?
|
|
104
|
+
|
|
105
|
+
payloads = array.flat_map.with_index do |value, index|
|
|
106
|
+
extract_payloads_from(value, source_type, [prefix, index].compact.join("."))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
unless Aikido::Zen.config.harden?
|
|
110
|
+
# Special case for File.join given a possibly nested array of strings,
|
|
111
|
+
# as might occur when a query parameter is an array.
|
|
112
|
+
begin
|
|
113
|
+
string = File.join__internal_for_aikido_zen(*array)
|
|
114
|
+
if unsafe_path?(string)
|
|
115
|
+
payloads << Payload.new(string, source_type, [prefix, "__File.join__"].compact.join("."))
|
|
116
|
+
end
|
|
117
|
+
rescue
|
|
118
|
+
# Could not create special payload for File.join.
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
payloads
|
|
104
123
|
else
|
|
105
|
-
Payload.new(data, source_type, prefix.to_s)
|
|
124
|
+
[Payload.new(data, source_type, prefix.to_s)]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def unsafe_path?(filepath)
|
|
129
|
+
normalized_filepath = Pathname.new(filepath).cleanpath.to_s.downcase
|
|
130
|
+
|
|
131
|
+
Scanners::PathTraversal::DANGEROUS_PATH_PARTS.each do |dangerous_path_part|
|
|
132
|
+
return true if normalized_filepath.include?(dangerous_path_part)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
Scanners::PathTraversal::DANGEROUS_PATH_STARTS.each do |dangerous_path_start|
|
|
136
|
+
return true if normalized_filepath.start_with?(dangerous_path_start)
|
|
106
137
|
end
|
|
138
|
+
|
|
139
|
+
false
|
|
107
140
|
end
|
|
108
141
|
end
|
|
109
142
|
end
|
|
@@ -14,53 +14,39 @@ module Aikido::Zen::DetachedAgent
|
|
|
14
14
|
# parent process. We want to have the freshest data.
|
|
15
15
|
#
|
|
16
16
|
# It's possible to use `extend Forwardable` here for one-line forward calls to the
|
|
17
|
-
# @
|
|
17
|
+
# @front_object object. Unfortunately, the methods to be called are
|
|
18
18
|
# created at runtime by `DRbObject`, which leads to an ugly warning about
|
|
19
19
|
# private methods after the delegator is bound.
|
|
20
20
|
class Agent
|
|
21
21
|
attr_reader :worker
|
|
22
22
|
|
|
23
23
|
def initialize(
|
|
24
|
+
config: Aikido::Zen.config,
|
|
25
|
+
worker: Aikido::Zen::Worker.new(config: config),
|
|
24
26
|
heartbeat_interval: 10,
|
|
25
27
|
polling_interval: 10,
|
|
26
|
-
|
|
27
|
-
collector: Aikido::Zen.collector,
|
|
28
|
-
worker: Aikido::Zen::Worker.new(config: config)
|
|
28
|
+
collector: Aikido::Zen.collector
|
|
29
29
|
)
|
|
30
30
|
@config = config
|
|
31
|
+
@worker = worker
|
|
31
32
|
@heartbeat_interval = heartbeat_interval
|
|
32
33
|
@polling_interval = polling_interval
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
@collector = collector
|
|
35
|
-
|
|
36
|
+
|
|
37
|
+
@front_object = DRbObject.new_with_uri(config.detached_agent_socket_uri)
|
|
38
|
+
|
|
36
39
|
@has_forked = false
|
|
37
40
|
schedule_tasks
|
|
38
41
|
end
|
|
39
42
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
heartbeat = @collector.flush(at: at)
|
|
44
|
-
@detached_agent_front.send_heartbeat_to_parent_process(heartbeat.as_json)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private def schedule_tasks
|
|
48
|
-
# For heartbeats is correct to send them from parent or child process. Otherwise, we'll lose
|
|
49
|
-
# stats made by the parent process.
|
|
50
|
-
@worker.every(@heartbeat_interval, run_now: false) { send_heartbeat }
|
|
51
|
-
|
|
52
|
-
# Runtime_settings fetch must happens only in the child processes, otherwise, due to
|
|
53
|
-
# we are updating the global runtime_settings, we could have an infinite recursion.
|
|
54
|
-
if @has_forked
|
|
55
|
-
@worker.every(@polling_interval) do
|
|
56
|
-
Aikido::Zen.runtime_settings = @detached_agent_front.updated_settings
|
|
57
|
-
@config.logger.debug "Updated runtime settings after polling from child process #{Process.pid}"
|
|
58
|
-
end
|
|
59
|
-
end
|
|
43
|
+
def send_collector_events
|
|
44
|
+
events_data = @collector.flush_events.map(&:as_json)
|
|
45
|
+
@front_object.send_collector_events(events_data)
|
|
60
46
|
end
|
|
61
47
|
|
|
62
48
|
def calculate_rate_limits(request)
|
|
63
|
-
@
|
|
49
|
+
@front_object.calculate_rate_limits(request.route.as_json, request.ip, request.actor.as_json)
|
|
64
50
|
end
|
|
65
51
|
|
|
66
52
|
# Every time a fork occurs (a new child process is created), we need to start
|
|
@@ -74,5 +60,20 @@ module Aikido::Zen::DetachedAgent
|
|
|
74
60
|
@worker.restart
|
|
75
61
|
schedule_tasks
|
|
76
62
|
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def schedule_tasks
|
|
67
|
+
@worker.every(@heartbeat_interval, run_now: false) { send_collector_events }
|
|
68
|
+
|
|
69
|
+
# Runtime_settings fetch must happens only in the child processes, otherwise, due to
|
|
70
|
+
# we are updating the global runtime_settings, we could have an infinite recursion.
|
|
71
|
+
if @has_forked
|
|
72
|
+
@worker.every(@polling_interval) do
|
|
73
|
+
Aikido::Zen.runtime_settings = @front_object.updated_settings
|
|
74
|
+
@config.logger.debug "Updated runtime settings after polling from child process #{Process.pid}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
77
78
|
end
|
|
78
79
|
end
|
|
@@ -7,20 +7,23 @@ module Aikido::Zen::DetachedAgent
|
|
|
7
7
|
class FrontObject
|
|
8
8
|
def initialize(
|
|
9
9
|
config: Aikido::Zen.config,
|
|
10
|
-
collector: Aikido::Zen.collector,
|
|
11
10
|
runtime_settings: Aikido::Zen.runtime_settings,
|
|
11
|
+
collector: Aikido::Zen.collector,
|
|
12
12
|
rate_limiter: Aikido::Zen::RateLimiter.new
|
|
13
13
|
)
|
|
14
14
|
@config = config
|
|
15
|
+
@runtime_settings = runtime_settings
|
|
15
16
|
@collector = collector
|
|
16
17
|
@rate_limiter = rate_limiter
|
|
17
|
-
@runtime_settings = runtime_settings
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
RequestKind = Struct.new(:route, :schema, :ip, :actor)
|
|
21
21
|
|
|
22
|
-
def
|
|
23
|
-
|
|
22
|
+
def send_collector_events(events_data)
|
|
23
|
+
events_data.each do |event_data|
|
|
24
|
+
event = Aikido::Zen::Collector::Event.from_json(event_data)
|
|
25
|
+
@collector.add_event(event)
|
|
26
|
+
end
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
# Method called by child processes to get an up-to-date version of the
|
|
@@ -29,8 +32,9 @@ module Aikido::Zen::DetachedAgent
|
|
|
29
32
|
@runtime_settings
|
|
30
33
|
end
|
|
31
34
|
|
|
32
|
-
def calculate_rate_limits(
|
|
33
|
-
actor = Aikido::Zen::Actor(
|
|
35
|
+
def calculate_rate_limits(route_data, ip, actor_data)
|
|
36
|
+
actor = Aikido::Zen::Actor.from_json(actor_data) if actor_data
|
|
37
|
+
route = Aikido::Zen::Route.from_json(route_data)
|
|
34
38
|
@rate_limiter.calculate_rate_limits(RequestKind.new(route, nil, ip, actor))
|
|
35
39
|
end
|
|
36
40
|
end
|
|
@@ -1,41 +1,78 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
3
5
|
module Aikido::Zen::DetachedAgent
|
|
4
6
|
class Server
|
|
7
|
+
# Initialize and start a detached agent server instance.
|
|
8
|
+
#
|
|
9
|
+
# @return [Aikido::Zen::DetachedAgent::Server]
|
|
10
|
+
def self.start(**opts)
|
|
11
|
+
new(**opts).tap(&:start!)
|
|
12
|
+
end
|
|
13
|
+
|
|
5
14
|
def initialize(config: Aikido::Zen.config)
|
|
6
|
-
@
|
|
7
|
-
@drb_server = DRb.start_service(config.detached_agent_socket_path, @detached_agent_front)
|
|
15
|
+
@started_at = nil
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
@drb_server.verbose = config.logger.debug?
|
|
11
|
-
end
|
|
17
|
+
@config = config
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
@
|
|
19
|
+
@socket_path = config.detached_agent_socket_path
|
|
20
|
+
@socket_uri = config.detached_agent_socket_uri
|
|
15
21
|
end
|
|
16
22
|
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
DRb.stop_service
|
|
23
|
+
def started?
|
|
24
|
+
!!@started_at
|
|
20
25
|
end
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
def start!
|
|
28
|
+
@config.logger.info("Starting DRb Server...")
|
|
29
|
+
|
|
30
|
+
# Try to ensure that the DRb service can start if the DRb service did
|
|
31
|
+
# not stop cleanly.
|
|
32
|
+
begin
|
|
33
|
+
# Check whether the Unix domain socket is in use by another process.
|
|
34
|
+
UNIXSocket.new(@socket_path).close
|
|
35
|
+
rescue Errno::ECONNREFUSED
|
|
36
|
+
@config.logger.debug("Removing residual Unix domain socket...")
|
|
37
|
+
|
|
38
|
+
# Remove the residual Unix domain socket.
|
|
39
|
+
FileUtils.rm_f(@socket_path)
|
|
40
|
+
rescue
|
|
41
|
+
# empty
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@front = FrontObject.new
|
|
45
|
+
|
|
46
|
+
# If the Unix domain socket is in use by another process and/or the
|
|
47
|
+
# residual Unix domain socket could not be removed DRb will raise an
|
|
48
|
+
# appropriate error.
|
|
49
|
+
@drb_server = DRb.start_service(@socket_uri, @front)
|
|
50
|
+
|
|
51
|
+
# Only show DRb output in debug mode.
|
|
52
|
+
@drb_server.verbose = @config.logger.debug?
|
|
53
|
+
|
|
54
|
+
# Ensure that the DRb server is alive.
|
|
55
|
+
max_attempts = 10
|
|
56
|
+
attempts = 0
|
|
57
|
+
until @drb_server.alive?
|
|
58
|
+
@config.logger.info("DRb Server still not alive. #{max_attempts - attempts} attempts remaining")
|
|
59
|
+
sleep 0.1
|
|
60
|
+
attempts += 1
|
|
61
|
+
raise Aikido::Zen::DetachedAgentError.new("Impossible to start the dRB server (socket=#{Aikido::Zen.config.detached_agent_socket_path})") \
|
|
62
|
+
if attempts == max_attempts
|
|
38
63
|
end
|
|
64
|
+
|
|
65
|
+
@started_at = Time.now.utc
|
|
66
|
+
|
|
67
|
+
at_exit { stop! if started? }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def stop!
|
|
71
|
+
@config.logger.info("Stopping DRb Server...")
|
|
72
|
+
@started_at = nil
|
|
73
|
+
|
|
74
|
+
@drb_server.stop_service if @drb_server.alive?
|
|
75
|
+
DRb.stop_service
|
|
39
76
|
end
|
|
40
77
|
end
|
|
41
78
|
end
|
data/lib/aikido/zen/event.rb
CHANGED
|
@@ -41,8 +41,10 @@ module Aikido::Zen
|
|
|
41
41
|
|
|
42
42
|
def as_json
|
|
43
43
|
super.update(
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
{
|
|
45
|
+
attack: @attack.as_json,
|
|
46
|
+
request: @attack.context&.request&.as_json
|
|
47
|
+
}.compact
|
|
46
48
|
)
|
|
47
49
|
end
|
|
48
50
|
end
|
|
@@ -67,5 +69,48 @@ module Aikido::Zen
|
|
|
67
69
|
)
|
|
68
70
|
end
|
|
69
71
|
end
|
|
72
|
+
|
|
73
|
+
class AttackWave < Event
|
|
74
|
+
# @param [Aikido::Zen::Context] a context
|
|
75
|
+
# @return [Aikido::Zen::Events::AttackWave] an attack wave event
|
|
76
|
+
def self.from_context(context)
|
|
77
|
+
request = Aikido::Zen::AttackWave::Request.new(
|
|
78
|
+
ip_address: context.request.client_ip,
|
|
79
|
+
user_agent: context.request.user_agent,
|
|
80
|
+
source: context.request.framework
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
attack = Aikido::Zen::AttackWave::Attack.new(
|
|
84
|
+
metadata: {}, # not used yet
|
|
85
|
+
user: context.request.actor
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
new(request: request, attack: attack)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [Aikido::Zen::AttackWave::Request]
|
|
92
|
+
attr_reader :request
|
|
93
|
+
|
|
94
|
+
# @return [Aikido::Zen::AttackWave::Attack]
|
|
95
|
+
attr_reader :attack
|
|
96
|
+
|
|
97
|
+
# @param [Aikido::Zen::AttackWave::Request] the attack wave request
|
|
98
|
+
# @param [Aikido::Zen::AttackWave::Attack] the attack wave attack
|
|
99
|
+
# @param opts [Hash<Symbol, Object>] any other options to pass to
|
|
100
|
+
# the superclass initializer.
|
|
101
|
+
# @return [Aikido::Zen::Events::AttackWave] an attack wave event
|
|
102
|
+
def initialize(request:, attack:, **opts)
|
|
103
|
+
super(type: "detected_attack_wave", **opts)
|
|
104
|
+
@request = request
|
|
105
|
+
@attack = attack
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def as_json
|
|
109
|
+
super.update(
|
|
110
|
+
request: @request.as_json,
|
|
111
|
+
attack: @attack.as_json
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
70
115
|
end
|
|
71
116
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido
|
|
4
|
+
module Zen
|
|
5
|
+
# @api private
|
|
6
|
+
module Helpers
|
|
7
|
+
# Normalizes a path by:
|
|
8
|
+
#
|
|
9
|
+
# 1. Collapsing consecutive forward slashes into a single forward slash.
|
|
10
|
+
# 2. Removing forward trailing slash, unless the normalized path is "/".
|
|
11
|
+
#
|
|
12
|
+
# @param path [String, nil] the path to normalize.
|
|
13
|
+
# @return [String, nil] the normalized path.
|
|
14
|
+
def self.normalize_path(path)
|
|
15
|
+
return path unless path
|
|
16
|
+
|
|
17
|
+
normalized_path = path.dup
|
|
18
|
+
normalized_path.squeeze!("/")
|
|
19
|
+
normalized_path.chomp!("/") unless normalized_path == "/"
|
|
20
|
+
normalized_path
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/aikido/zen/internals.rb
CHANGED
|
@@ -60,11 +60,11 @@ module Aikido::Zen
|
|
|
60
60
|
# @!method self.detect_sql_injection_native(query, input, dialect)
|
|
61
61
|
# @param (see .detect_sql_injection)
|
|
62
62
|
# @returns [Integer] 0 if no injection detected, 1 if an injection was
|
|
63
|
-
# detected,
|
|
63
|
+
# detected, 2 if there was an internal error, or 3 if SQL tokenization failed.
|
|
64
64
|
# @raise [Aikido::Zen::InternalsError] if there's a problem loading or
|
|
65
65
|
# calling libzen.
|
|
66
66
|
attach_function :detect_sql_injection_native, :detect_sql_injection,
|
|
67
|
-
[:
|
|
67
|
+
[:pointer, :size_t, :pointer, :size_t, :int], :int
|
|
68
68
|
rescue LoadError, FFI::NotFoundError => err # rubocop:disable Lint/ShadowedException
|
|
69
69
|
# :nocov:
|
|
70
70
|
|
|
@@ -90,14 +90,34 @@ module Aikido::Zen
|
|
|
90
90
|
# @raise [Aikido::Zen::InternalsError] if there's a problem loading or
|
|
91
91
|
# calling libzen.
|
|
92
92
|
def self.detect_sql_injection(query, input, dialect)
|
|
93
|
-
|
|
93
|
+
query_bytes = encode_safely(query)
|
|
94
|
+
input_bytes = encode_safely(input)
|
|
95
|
+
|
|
96
|
+
query_ptr = FFI::MemoryPointer.new(:uint8, query_bytes.bytesize)
|
|
97
|
+
input_ptr = FFI::MemoryPointer.new(:uint8, input_bytes.bytesize)
|
|
98
|
+
|
|
99
|
+
query_ptr.put_bytes(0, query_bytes)
|
|
100
|
+
input_ptr.put_bytes(0, input_bytes)
|
|
101
|
+
|
|
102
|
+
case detect_sql_injection_native(query_ptr, query_bytes.bytesize, input_ptr, input_bytes.bytesize, dialect)
|
|
94
103
|
when 0 then false
|
|
95
104
|
when 1 then true
|
|
96
105
|
when 2
|
|
97
106
|
attempt = format("%s query %p with input %p", dialect, query, input)
|
|
98
107
|
raise InternalsError.new(attempt, "calling detect_sql_injection in", libzen_name)
|
|
108
|
+
when 3
|
|
109
|
+
# SQL tokenization failed - return false (no injection detected)
|
|
110
|
+
false
|
|
99
111
|
end
|
|
100
112
|
end
|
|
101
113
|
end
|
|
114
|
+
|
|
115
|
+
class << self
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def encode_safely(string)
|
|
119
|
+
string.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
102
122
|
end
|
|
103
123
|
end
|
|
Binary file
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Aikido::Zen
|
|
4
4
|
module Middleware
|
|
5
5
|
# Middleware that rejects requests from IPs blocked in the Aikido dashboard.
|
|
6
|
-
class
|
|
6
|
+
class AllowedAddressChecker
|
|
7
7
|
def initialize(app, config: Aikido::Zen.config, settings: Aikido::Zen.runtime_settings)
|
|
8
8
|
@app = app
|
|
9
9
|
@config = config
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido
|
|
4
|
+
module Zen
|
|
5
|
+
module Middleware
|
|
6
|
+
class AttackWaveProtector
|
|
7
|
+
def initialize(app, zen: Aikido::Zen, settings: Aikido::Zen.runtime_settings)
|
|
8
|
+
@app = app
|
|
9
|
+
@zen = zen
|
|
10
|
+
@settings = settings
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
response = @app.call(env)
|
|
15
|
+
|
|
16
|
+
context = @zen.current_context
|
|
17
|
+
protect(context)
|
|
18
|
+
|
|
19
|
+
response
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @api private
|
|
23
|
+
# Visible for testing.
|
|
24
|
+
def attack_wave?(context)
|
|
25
|
+
request = context.request
|
|
26
|
+
return false if request.nil?
|
|
27
|
+
|
|
28
|
+
# Bypass attack wave protection for allowed IPs
|
|
29
|
+
return false if @settings.allowed_ips.include?(request.ip)
|
|
30
|
+
|
|
31
|
+
@zen.attack_wave_detector.attack_wave?(context)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @api private
|
|
35
|
+
# Visible for testing.
|
|
36
|
+
def protect(context)
|
|
37
|
+
if attack_wave?(context)
|
|
38
|
+
attack_wave = Aikido::Zen::Events::AttackWave.from_context(context)
|
|
39
|
+
@zen.track_attack_wave(attack_wave)
|
|
40
|
+
@zen.agent.report(attack_wave)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -9,7 +9,7 @@ module Aikido::Zen
|
|
|
9
9
|
module Middleware
|
|
10
10
|
# Rack middleware that keeps the current context in a Thread/Fiber-local
|
|
11
11
|
# variable so that other parts of the agent/firewall can access it.
|
|
12
|
-
class
|
|
12
|
+
class ContextSetter
|
|
13
13
|
def initialize(app)
|
|
14
14
|
@app = app
|
|
15
15
|
end
|