aikido-zen 1.0.2.beta.5-x86_64-linux → 1.0.2.beta.6-x86_64-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.
- checksums.yaml +4 -4
- data/.simplecov +6 -0
- data/README.md +1 -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/rails.md +5 -7
- data/lib/aikido/zen/actor.rb +34 -4
- data/lib/aikido/zen/agent/heartbeats_manager.rb +5 -5
- data/lib/aikido/zen/agent.rb +17 -15
- data/lib/aikido/zen/collector/event.rb +209 -0
- data/lib/aikido/zen/collector/routes.rb +13 -8
- data/lib/aikido/zen/collector/stats.rb +16 -19
- data/lib/aikido/zen/collector/users.rb +3 -1
- data/lib/aikido/zen/collector.rb +94 -29
- data/lib/aikido/zen/config.rb +4 -10
- data/lib/aikido/zen/context.rb +8 -7
- 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/internals.rb +23 -3
- data/lib/aikido/zen/libzen-v0.1.48-x86_64-linux.so +0 -0
- data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
- data/lib/aikido/zen/middleware/request_tracker.rb +1 -1
- data/lib/aikido/zen/outbound_connection.rb +7 -0
- data/lib/aikido/zen/rails_engine.rb +2 -6
- data/lib/aikido/zen/route.rb +7 -0
- data/lib/aikido/zen/runtime_settings.rb +1 -1
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +10 -7
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +2 -2
- data/lib/aikido/zen/sink.rb +1 -1
- data/lib/aikido/zen/sinks/file.rb +43 -4
- data/lib/aikido/zen/sinks/kernel.rb +1 -1
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen.rb +8 -9
- metadata +6 -3
- data/lib/aikido/zen/libzen-v0.1.39-x86_64-linux.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 [
|
|
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 [
|
|
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 [
|
|
58
|
+
# @return [void]
|
|
60
59
|
def add_request
|
|
61
60
|
@requests += 1
|
|
62
|
-
self
|
|
63
61
|
end
|
|
64
62
|
|
|
65
|
-
# @param
|
|
66
|
-
# @
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
71
|
-
stats.add_timing(
|
|
72
|
-
self
|
|
70
|
+
stats.errors += 1 if has_errors
|
|
71
|
+
stats.add_timing(duration)
|
|
73
72
|
end
|
|
74
73
|
|
|
75
|
-
# @param
|
|
76
|
-
# @param being_blocked [Boolean] whether the Agent blocked the
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
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]
|
|
18
|
+
self[actor.id] |= actor
|
|
17
19
|
else
|
|
18
20
|
self[actor.id] = actor
|
|
19
21
|
end
|
data/lib/aikido/zen/collector.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
83
|
+
def track_request
|
|
84
|
+
add_event(Events::TrackRequest.new)
|
|
62
85
|
end
|
|
63
86
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
143
|
+
# Record the visited endpoint, and if enabled, the API schema for this endpoint.
|
|
97
144
|
#
|
|
98
|
-
# @param
|
|
145
|
+
# @param request [Aikido::Zen::Request]
|
|
99
146
|
# @return [void]
|
|
100
|
-
def
|
|
101
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
data/lib/aikido/zen/config.rb
CHANGED
|
@@ -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
|
|
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.
|
|
@@ -167,7 +161,7 @@ module Aikido::Zen
|
|
|
167
161
|
self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
|
|
168
162
|
self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
|
|
169
163
|
self.polling_interval = 60
|
|
170
|
-
self.
|
|
164
|
+
self.initial_heartbeat_delays = [30, 60 * 2]
|
|
171
165
|
self.json_encoder = DEFAULT_JSON_ENCODER
|
|
172
166
|
self.json_decoder = DEFAULT_JSON_DECODER
|
|
173
167
|
self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
|
data/lib/aikido/zen/context.rb
CHANGED
|
@@ -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
|
|
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.flat_map.with_index
|
|
102
|
-
extract_payloads_from(
|
|
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
|
-
# @
|
|
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)
|
|
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
|
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
|
|
@@ -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
|
|
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
|
data/lib/aikido/zen/route.rb
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 =
|
|
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"])
|