aikido-zen 1.0.2.beta.2-arm64-linux-musl → 1.0.2.beta.6-arm64-linux-musl

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +6 -0
  3. data/README.md +1 -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/rails.md +5 -7
  8. data/lib/aikido/zen/actor.rb +34 -4
  9. data/lib/aikido/zen/agent/heartbeats_manager.rb +5 -5
  10. data/lib/aikido/zen/agent.rb +17 -15
  11. data/lib/aikido/zen/attack.rb +12 -4
  12. data/lib/aikido/zen/collector/event.rb +209 -0
  13. data/lib/aikido/zen/collector/routes.rb +13 -8
  14. data/lib/aikido/zen/collector/stats.rb +16 -19
  15. data/lib/aikido/zen/collector/users.rb +3 -1
  16. data/lib/aikido/zen/collector.rb +94 -29
  17. data/lib/aikido/zen/config.rb +14 -13
  18. data/lib/aikido/zen/context.rb +8 -7
  19. data/lib/aikido/zen/detached_agent/agent.rb +28 -27
  20. data/lib/aikido/zen/detached_agent/front_object.rb +10 -6
  21. data/lib/aikido/zen/internals.rb +23 -3
  22. data/lib/aikido/zen/libzen-v0.1.48-arm64-linux-musl.so +0 -0
  23. data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
  24. data/lib/aikido/zen/middleware/request_tracker.rb +1 -1
  25. data/lib/aikido/zen/outbound_connection.rb +7 -0
  26. data/lib/aikido/zen/rails_engine.rb +2 -6
  27. data/lib/aikido/zen/route.rb +7 -0
  28. data/lib/aikido/zen/runtime_settings.rb +1 -1
  29. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +10 -7
  30. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +2 -2
  31. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +3 -1
  32. data/lib/aikido/zen/sink.rb +1 -1
  33. data/lib/aikido/zen/sinks/file.rb +43 -4
  34. data/lib/aikido/zen/sinks/kernel.rb +1 -1
  35. data/lib/aikido/zen/version.rb +2 -2
  36. data/lib/aikido/zen.rb +8 -9
  37. metadata +6 -3
  38. data/lib/aikido/zen/libzen-v0.1.39-arm64-linux-musl.so +0 -0
@@ -35,10 +35,9 @@ module Aikido::Zen
35
35
  # Track the timestamp we start tracking this series of stats.
36
36
  #
37
37
  # @param at [Time]
38
- # @return [self]
38
+ # @return [void]
39
39
  def start(at = Time.now.utc)
40
40
  @started_at = at
41
- self
42
41
  end
43
42
 
44
43
  # Sets the end time for these stats block, freezes it to avoid any more
@@ -47,7 +46,7 @@ module Aikido::Zen
47
46
  #
48
47
  # @param at [Time] the time at which we're resetting, which is set as the
49
48
  # ending time for the returned copy.
50
- # @return [self]
49
+ # @return [void]
51
50
  def flush(at: Time.now.utc)
52
51
  # Make sure the timing stats are compressed before copying, since we
53
52
  # need these compressed when we serialize this for the API.
@@ -56,31 +55,29 @@ module Aikido::Zen
56
55
  freeze
57
56
  end
58
57
 
59
- # @return [self]
58
+ # @return [void]
60
59
  def add_request
61
60
  @requests += 1
62
- self
63
61
  end
64
62
 
65
- # @param scan [Aikido::Zen::Scan]
66
- # @return [self]
67
- def add_scan(scan)
68
- stats = @sinks[scan.sink.name]
63
+ # @param sink_name [String] the name of the sink
64
+ # @param duration [Float] the length the scan in seconds
65
+ # @param has_errors [Boolean] whether errors occurred during the scan
66
+ # @return [void]
67
+ def add_scan(sink_name, duration, has_errors:)
68
+ stats = @sinks[sink_name]
69
69
  stats.scans += 1
70
- stats.errors += 1 if scan.errors?
71
- stats.add_timing(scan.duration)
72
- self
70
+ stats.errors += 1 if has_errors
71
+ stats.add_timing(duration)
73
72
  end
74
73
 
75
- # @param attack [Aikido::Zen::Attack]
76
- # @param being_blocked [Boolean] whether the Agent blocked the
77
- # request where this Attack happened or not.
78
- # @return [self]
79
- def add_attack(attack, being_blocked:)
80
- stats = @sinks[attack.sink.name]
74
+ # @param sink_name [String] the name of the sink
75
+ # @param being_blocked [Boolean] whether the Agent blocked the request
76
+ # @return [void]
77
+ def add_attack(sink_name, being_blocked:)
78
+ stats = @sinks[sink_name]
81
79
  stats.attacks += 1
82
80
  stats.blocked_attacks += 1 if being_blocked
83
- self
84
81
  end
85
82
 
86
83
  def as_json
@@ -11,9 +11,11 @@ module Aikido::Zen
11
11
  super(config.max_users_tracked)
12
12
  end
13
13
 
14
+ # @param actor [Aikido::Zen::Actor]
15
+ # @return [void]
14
16
  def add(actor)
15
17
  if key?(actor.id)
16
- self[actor.id].update
18
+ self[actor.id] |= actor
17
19
  else
18
20
  self[actor.id] = actor
19
21
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "collector/event"
4
+
3
5
  module Aikido::Zen
4
6
  # Handles collecting all the runtime statistics to report back to the Aikido
5
7
  # servers.
@@ -7,14 +9,44 @@ module Aikido::Zen
7
9
  def initialize(config: Aikido::Zen.config)
8
10
  @config = config
9
11
 
12
+ @events = Queue.new
13
+
10
14
  @stats = Concurrent::AtomicReference.new(Stats.new(@config))
11
15
  @users = Concurrent::AtomicReference.new(Users.new(@config))
12
16
  @hosts = Concurrent::AtomicReference.new(Hosts.new(@config))
13
17
  @routes = Concurrent::AtomicReference.new(Routes.new(@config))
14
- @heartbeats = Queue.new
18
+
15
19
  @middleware_installed = Concurrent::AtomicBoolean.new
16
20
  end
17
21
 
22
+ # Add an event, to be handled in the collector in current process or sent
23
+ # to a collector in another process.
24
+ #
25
+ # @param event [Aikido::Zen::Collector::Event] the event to add
26
+ # @return [void]
27
+ def add_event(event)
28
+ @events << event
29
+ end
30
+
31
+ # @return [Boolean] whether the collector has any events
32
+ def has_events?
33
+ !@events.empty?
34
+ end
35
+
36
+ # Flush all events.
37
+ #
38
+ # @return [Array<Aikido::Zen::Collector::Event>]
39
+ def flush_events
40
+ Array.new(@events.size) { @events.pop }
41
+ end
42
+
43
+ # Handle the events in the queue.
44
+ #
45
+ # @return [void]
46
+ def handle
47
+ flush_events.each { |event| event.handle(self) }
48
+ end
49
+
18
50
  # Flush all the stats into a Heartbeat event that can be reported back to
19
51
  # the Aikido servers.
20
52
  #
@@ -22,6 +54,8 @@ module Aikido::Zen
22
54
  # of the new stats collection period. Defaults to now.
23
55
  # @return [Aikido::Zen::Events::Heartbeat]
24
56
  def flush(at: Time.now.utc)
57
+ handle
58
+
25
59
  stats = @stats.get_and_set(Stats.new(@config))
26
60
  users = @users.get_and_set(Users.new(@config))
27
61
  hosts = @hosts.get_and_set(Hosts.new(@config))
@@ -30,21 +64,11 @@ module Aikido::Zen
30
64
  start(at: at)
31
65
  stats = stats.flush(at: at)
32
66
 
33
- Events::Heartbeat.new(
67
+ Aikido::Zen::Events::Heartbeat.new(
34
68
  stats: stats, users: users, hosts: hosts, routes: routes, middleware_installed: middleware_installed?
35
69
  )
36
70
  end
37
71
 
38
- # Put heartbeats coming from child processes into the internal queue.
39
- def push_heartbeat(heartbeat)
40
- @heartbeats << heartbeat
41
- end
42
-
43
- # Drains into an array all the queued heartbeats
44
- def flush_heartbeats
45
- Array.new(@heartbeats.size) { @heartbeats.pop }
46
- end
47
-
48
72
  # Sets the start time for this collection period.
49
73
  #
50
74
  # @param at [Time] defaults to now.
@@ -55,16 +79,13 @@ module Aikido::Zen
55
79
 
56
80
  # Track stats about the requests
57
81
  #
58
- # @param request [Aikido::Zen::Request]
59
82
  # @return [void]
60
- def track_request(*)
61
- synchronize(@stats) { |stats| stats.add_request }
83
+ def track_request
84
+ add_event(Events::TrackRequest.new)
62
85
  end
63
86
 
64
- # Record the visited endpoint, and if enabled, the API schema for this endpoint.
65
- # @param request [Aikido::Zen::Request]
66
- def track_route(request)
67
- synchronize(@routes) { |routes| routes.add(request) if request.route }
87
+ def handle_track_request
88
+ synchronize(@stats) { |stats| stats.add_request }
68
89
  end
69
90
 
70
91
  # Track stats about a scan performed by one of our sinks.
@@ -72,7 +93,13 @@ module Aikido::Zen
72
93
  # @param scan [Aikido::Zen::Scan]
73
94
  # @return [void]
74
95
  def track_scan(scan)
75
- synchronize(@stats) { |stats| stats.add_scan(scan) }
96
+ add_event(Events::TrackScan.new(scan.sink.name, scan.duration, has_errors: scan.errors?))
97
+ end
98
+
99
+ def handle_track_scan(sink_name, duration, has_errors:)
100
+ synchronize(@stats) do |stats|
101
+ stats.add_scan(sink_name, duration, has_errors: has_errors)
102
+ end
76
103
  end
77
104
 
78
105
  # Track stats about an attack detected by our scanners.
@@ -80,25 +107,49 @@ module Aikido::Zen
80
107
  # @param attack [Aikido::Zen::Attack]
81
108
  # @return [void]
82
109
  def track_attack(attack)
110
+ add_event(Events::TrackAttack.new(attack.sink.name, being_blocked: attack.blocked?))
111
+ end
112
+
113
+ def handle_track_attack(sink_name, being_blocked:)
83
114
  synchronize(@stats) do |stats|
84
- stats.add_attack(attack, being_blocked: attack.blocked?)
115
+ stats.add_attack(sink_name, being_blocked: being_blocked)
85
116
  end
86
117
  end
87
118
 
119
+ # Track the user reported by the developer to be behind this request.
120
+ #
121
+ # @param actor [Aikido::Zen::Actor]
122
+ # @return [void]
123
+ def track_user(actor)
124
+ add_event(Events::TrackUser.new(actor))
125
+ end
126
+
127
+ def handle_track_user(actor)
128
+ synchronize(@users) { |users| users.add(actor) }
129
+ end
130
+
88
131
  # Track an HTTP connections to an external host.
89
132
  #
90
133
  # @param connection [Aikido::Zen::OutboundConnection]
91
134
  # @return [void]
92
135
  def track_outbound(connection)
136
+ add_event(Events::TrackOutbound.new(connection))
137
+ end
138
+
139
+ def handle_track_outbound(connection)
93
140
  synchronize(@hosts) { |hosts| hosts.add(connection) }
94
141
  end
95
142
 
96
- # Track the user reported by the developer to be behind this request.
143
+ # Record the visited endpoint, and if enabled, the API schema for this endpoint.
97
144
  #
98
- # @param actor [Aikido::Zen::Actor]
145
+ # @param request [Aikido::Zen::Request]
99
146
  # @return [void]
100
- def track_user(actor)
101
- synchronize(@users) { |users| users.add(actor) }
147
+ def track_route(request)
148
+ add_event(Events::TrackRoute.new(request.route, request.schema))
149
+ end
150
+
151
+ def handle_track_route(route, schema)
152
+ synchronize(@routes) { |routes| routes.add(route, schema) }
102
153
  end
103
154
 
104
155
  def middleware_installed!
@@ -106,26 +157,40 @@ module Aikido::Zen
106
157
  end
107
158
 
108
159
  # @api private
109
- def routes
110
- @routes.get
160
+ #
161
+ # @note Visible for testing.
162
+ def stats
163
+ handle
164
+ @stats.get
111
165
  end
112
166
 
113
167
  # @api private
168
+ #
169
+ # @note Visible for testing.
114
170
  def users
171
+ handle
115
172
  @users.get
116
173
  end
117
174
 
118
175
  # @api private
176
+ #
177
+ # @note Visible for testing.
119
178
  def hosts
179
+ handle
120
180
  @hosts.get
121
181
  end
122
182
 
123
183
  # @api private
124
- def stats
125
- @stats.get
184
+ #
185
+ # @note Visible for testing.
186
+ def routes
187
+ handle
188
+ @routes.get
126
189
  end
127
190
 
128
191
  # @api private
192
+ #
193
+ # @note Visible for testing.
129
194
  def middleware_installed?
130
195
  @middleware_installed.true?
131
196
  end
@@ -8,12 +8,6 @@ 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
@@ -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.
@@ -70,6 +64,9 @@ module Aikido::Zen
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,13 +143,16 @@ 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
 
153
- # @return [String] environment specific HTTP header providing the client IP.
154
- attr_accessor :client_ip_header
155
-
156
156
  def initialize
157
157
  self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
158
158
  self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
@@ -161,7 +161,7 @@ module Aikido::Zen
161
161
  self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
162
162
  self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
163
163
  self.polling_interval = 60
164
- self.initial_heartbeat_delay = 60
164
+ self.initial_heartbeat_delays = [30, 60 * 2]
165
165
  self.json_encoder = DEFAULT_JSON_ENCODER
166
166
  self.json_decoder = DEFAULT_JSON_DECODER
167
167
  self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
@@ -183,6 +183,7 @@ module Aikido::Zen
183
183
  self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
184
184
  self.api_schema_collection_max_depth = 20
185
185
  self.api_schema_collection_max_properties = 20
186
+ self.stored_ssrf = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_STORED_SSRF", true))
186
187
  self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
187
188
  end
188
189
 
@@ -92,17 +92,18 @@ 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
+ data.to_ary.flat_map.with_index do |value, index|
103
+ extract_payloads_from(value, source_type, [prefix, index].compact.join("."))
104
+ end
104
105
  else
105
- Payload.new(data, source_type, prefix.to_s)
106
+ [Payload.new(data, source_type, prefix.to_s)]
106
107
  end
107
108
  end
108
109
  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_uri)
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)
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
@@ -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
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido
4
+ module Zen
5
+ module Middleware
6
+ # This middleware is responsible for detecting when a process has forked
7
+ # (e.g., in a Puma or Unicorn worker) and resetting the state of the
8
+ # Aikido Zen agent. It should be inserted early in the middleware stack.
9
+ class ForkDetector
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ # This is the single, reliable trigger point for the fork check.
16
+ Aikido::Zen.check_and_handle_fork
17
+
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -18,7 +18,7 @@ module Aikido::Zen
18
18
  route: request.route.path,
19
19
  http_method: request.request_method
20
20
  )
21
- Aikido::Zen.track_request request
21
+ Aikido::Zen.track_request(request)
22
22
 
23
23
  if Aikido::Zen.config.collect_api_schema?
24
24
  Aikido::Zen.track_discovered_route(request)
@@ -3,6 +3,13 @@
3
3
  module Aikido::Zen
4
4
  # Simple data object to identify connections performed to outbound servers.
5
5
  class OutboundConnection
6
+ def self.from_json(data)
7
+ new(
8
+ host: data[:hostname],
9
+ port: data[:port]
10
+ )
11
+ end
12
+
6
13
  # Convenience factory to create connection descriptions out of URI objects.
7
14
  #
8
15
  # @param uri [URI]
@@ -10,6 +10,8 @@ module Aikido::Zen
10
10
  end
11
11
 
12
12
  initializer "aikido.add_middleware" do |app|
13
+ app.middleware.insert_before 0, Aikido::Zen::Middleware::ForkDetector
14
+
13
15
  app.middleware.use Aikido::Zen::Middleware::SetContext
14
16
  app.middleware.use Aikido::Zen::Middleware::CheckAllowedAddresses
15
17
  # Request Tracker stats do not consider failed request or 40x, so the middleware
@@ -29,12 +31,6 @@ module Aikido::Zen
29
31
  end
30
32
 
31
33
  initializer "aikido.configuration" do |app|
32
- # Allow the logger to be configured before checking if disabled? so we can
33
- # let the user know that the agent is disabled.
34
- logger = ::Rails.logger
35
- logger = logger.tagged("aikido") if logger.respond_to?(:tagged)
36
- app.config.zen.logger = logger
37
-
38
34
  app.config.zen.request_builder = Aikido::Zen::Context::RAILS_REQUEST_BUILDER
39
35
 
40
36
  # Plug Rails' JSON encoder/decoder, but only if the user hasn't changed
@@ -5,6 +5,13 @@ module Aikido::Zen
5
5
  # framework to go from a given HTTP request to the code that handles said
6
6
  # request.
7
7
  class Route
8
+ def self.from_json(data)
9
+ new(
10
+ verb: data[:method],
11
+ path: data[:path]
12
+ )
13
+ end
14
+
8
15
  # @return [String] the HTTP verb used to request this route.
9
16
  attr_reader :verb
10
17
 
@@ -50,7 +50,7 @@ module Aikido::Zen
50
50
  last_updated_at = updated_at
51
51
 
52
52
  self.updated_at = Time.at(data["configUpdatedAt"].to_i / 1000)
53
- self.heartbeat_interval = (data["heartbeatIntervalInMS"].to_i / 1000)
53
+ self.heartbeat_interval = data["heartbeatIntervalInMS"].to_i / 1000
54
54
  self.endpoints = RuntimeSettings::Endpoints.from_json(data["endpoints"])
55
55
  self.blocked_user_ids = data["blockedUserIds"]
56
56
  self.skip_protection_for_ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])