aikido-zen 1.0.1.beta.5-arm64-linux → 1.0.2-arm64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +6 -0
  3. data/README.md +2 -0
  4. data/benchmarks/README.md +0 -1
  5. data/benchmarks/rails7.1_benchmark.js +1 -0
  6. data/benchmarks/rails7.1_sql_injection.js +52 -20
  7. data/docs/config.md +9 -1
  8. data/docs/proxy.md +10 -0
  9. data/docs/rails.md +55 -13
  10. data/docs/troubleshooting.md +62 -0
  11. data/lib/aikido/zen/actor.rb +34 -4
  12. data/lib/aikido/zen/agent/heartbeats_manager.rb +5 -5
  13. data/lib/aikido/zen/agent.rb +19 -17
  14. data/lib/aikido/zen/attack.rb +19 -9
  15. data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
  16. data/lib/aikido/zen/attack_wave.rb +88 -0
  17. data/lib/aikido/zen/cache.rb +91 -0
  18. data/lib/aikido/zen/capped_collections.rb +22 -4
  19. data/lib/aikido/zen/collector/event.rb +238 -0
  20. data/lib/aikido/zen/collector/hosts.rb +16 -1
  21. data/lib/aikido/zen/collector/routes.rb +13 -8
  22. data/lib/aikido/zen/collector/stats.rb +33 -22
  23. data/lib/aikido/zen/collector/users.rb +5 -3
  24. data/lib/aikido/zen/collector.rb +107 -28
  25. data/lib/aikido/zen/config.rb +54 -21
  26. data/lib/aikido/zen/context/rack_request.rb +3 -0
  27. data/lib/aikido/zen/context/rails_request.rb +3 -0
  28. data/lib/aikido/zen/context.rb +42 -9
  29. data/lib/aikido/zen/detached_agent/agent.rb +28 -27
  30. data/lib/aikido/zen/detached_agent/front_object.rb +10 -6
  31. data/lib/aikido/zen/detached_agent/server.rb +63 -26
  32. data/lib/aikido/zen/event.rb +47 -2
  33. data/lib/aikido/zen/helpers.rb +24 -0
  34. data/lib/aikido/zen/internals.rb +23 -3
  35. data/lib/aikido/zen/libzen-v0.1.48-arm64-linux.so +0 -0
  36. data/lib/aikido/zen/middleware/{check_allowed_addresses.rb → allowed_address_checker.rb} +1 -1
  37. data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
  38. data/lib/aikido/zen/middleware/{set_context.rb → context_setter.rb} +1 -1
  39. data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
  40. data/lib/aikido/zen/middleware/rack_throttler.rb +3 -1
  41. data/lib/aikido/zen/middleware/request_tracker.rb +9 -4
  42. data/lib/aikido/zen/outbound_connection.rb +18 -1
  43. data/lib/aikido/zen/payload.rb +1 -1
  44. data/lib/aikido/zen/rails_engine.rb +5 -8
  45. data/lib/aikido/zen/request/rails_router.rb +17 -2
  46. data/lib/aikido/zen/request.rb +21 -36
  47. data/lib/aikido/zen/route.rb +57 -0
  48. data/lib/aikido/zen/runtime_settings/endpoints.rb +37 -8
  49. data/lib/aikido/zen/runtime_settings.rb +6 -5
  50. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +10 -7
  51. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +5 -4
  52. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +3 -2
  53. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +3 -2
  54. data/lib/aikido/zen/scanners/ssrf_scanner.rb +2 -1
  55. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +8 -2
  56. data/lib/aikido/zen/sink.rb +1 -1
  57. data/lib/aikido/zen/sinks/action_controller.rb +3 -1
  58. data/lib/aikido/zen/sinks/file.rb +45 -4
  59. data/lib/aikido/zen/sinks/kernel.rb +1 -1
  60. data/lib/aikido/zen/sinks/socket.rb +7 -0
  61. data/lib/aikido/zen/sinks_dsl.rb +12 -0
  62. data/lib/aikido/zen/system_info.rb +1 -5
  63. data/lib/aikido/zen/version.rb +2 -2
  64. data/lib/aikido/zen.rb +77 -15
  65. data/tasklib/bench.rake +1 -1
  66. data/tasklib/libzen.rake +1 -0
  67. metadata +15 -5
  68. data/lib/aikido/zen/libzen-v0.1.39-arm64-linux.so +0 -0
@@ -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 AIKIDO_DISABLED environment variable.
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 amount in seconds to wait before sending an initial
50
- # heartbeat event when the server reports no stats have been sent yet.
51
- attr_accessor :initial_heartbeat_delay
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 [string] Path of the socket where the detached agent will listen.
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
- attr_reader :detached_agent_socket_path
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.initial_heartbeat_delay = 60
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 = 1800 # 30 min
176
- self.client_rate_limit_period = 3600 # 1 hour
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 detached_agent_socket_path=(path)
226
- @detached_agent_socket_path = 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)
@@ -81,8 +81,8 @@ module Aikido::Zen
81
81
  def protection_disabled?
82
82
  return false if request.nil?
83
83
 
84
- !@settings.endpoints[request.route].protected? ||
85
- @settings.skip_protection_for_ips.include?(request.ip)
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 { |name, val|
98
- extract_payloads_from(val, source_type, [prefix, name].compact.join("."))
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.flat_map.with_index { |val, idx|
102
- extract_payloads_from(val, source_type, [prefix, idx].compact.join("."))
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
- # @detached_agent_front object. Unfortunately, the methods to be called are
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
- config: Aikido::Zen.config,
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
- @worker = worker
34
+
34
35
  @collector = collector
35
- @detached_agent_front = DRbObject.new_with_uri(config.detached_agent_socket_path)
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 send_heartbeat(at: Time.now.utc)
41
- return unless @collector.stats.any?
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
- @detached_agent_front.calculate_rate_limits(request.route, request.ip, request.actor.to_json)
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 send_heartbeat_to_parent_process(heartbeat)
23
- @collector.push_heartbeat(heartbeat)
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(route, ip, actor_hash)
33
- actor = Aikido::Zen::Actor(actor_hash) if actor_hash
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
- @detached_agent_front = FrontObject.new
7
- @drb_server = DRb.start_service(config.detached_agent_socket_path, @detached_agent_front)
15
+ @started_at = nil
8
16
 
9
- # We don't want to see drb logs unless in debug mode
10
- @drb_server.verbose = config.logger.debug?
11
- end
17
+ @config = config
12
18
 
13
- def alive?
14
- @drb_server.alive?
19
+ @socket_path = config.detached_agent_socket_path
20
+ @socket_uri = config.detached_agent_socket_uri
15
21
  end
16
22
 
17
- def stop!
18
- @drb_server.stop_service
19
- DRb.stop_service
23
+ def started?
24
+ !!@started_at
20
25
  end
21
26
 
22
- class << self
23
- def start!
24
- Aikido::Zen.config.logger.debug("Starting DRb Server...")
25
- max_attempts = 10
26
- @server = new
27
-
28
- attempts = 0
29
- until @server.alive?
30
- Aikido::Zen.config.logger.info("DRb Server still not alive. #{max_attempts - attempts} attempts remaining")
31
- sleep 0.1
32
- attempts += 1
33
- raise Aikido::Zen::DetachedAgentError.new("Impossible to start the dRB server (socket=#{Aikido::Zen.config.detached_agent_socket_path})") \
34
- if attempts == max_attempts
35
- end
36
-
37
- @server
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
@@ -41,8 +41,10 @@ module Aikido::Zen
41
41
 
42
42
  def as_json
43
43
  super.update(
44
- attack: @attack.as_json,
45
- request: @attack.context.request.as_json
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
@@ -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, or 2 if there was an internal error.
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
- [:string, :string, :int], :int
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
- case detect_sql_injection_native(query, input, dialect)
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
@@ -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 CheckAllowedAddresses
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 SetContext
12
+ class ContextSetter
13
13
  def initialize(app)
14
14
  @app = app
15
15
  end