aikido-zen 1.4.1 → 1.5.1
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/lib/aikido/zen/agent.rb +69 -12
- data/lib/aikido/zen/api_client.rb +8 -2
- data/lib/aikido/zen/api_stream.rb +192 -0
- data/lib/aikido/zen/attack_wave/helpers.rb +14 -5
- data/lib/aikido/zen/attack_wave.rb +2 -2
- data/lib/aikido/zen/config.rb +6 -0
- data/lib/aikido/zen/context.rb +4 -2
- data/lib/aikido/zen/middleware/attack_wave_protector.rb +6 -5
- data/lib/aikido/zen/runtime_settings.rb +5 -3
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +3 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +1 -1
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +1 -1
- data/lib/aikido/zen/version.rb +1 -1
- data/lib/aikido/zen.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 060fe3aabca1fbc463fc77129a0b535d6c53f2c78bf5c4d9d11c2bdd46d119be
|
|
4
|
+
data.tar.gz: 1fa12dcaed6348cfc9ed836e04c995f2459c46b23c8ba5c529fe5fa0c01da40e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8d1311f2b1ab68a068b5aadf5356f6d092ac9df323030c7143d042e9a9705cf7efa06efcf7f15804c7174f78543ad0d630d53b47d42fce487de8f2eeec344eb
|
|
7
|
+
data.tar.gz: cd12d37d110b93b2603f87897985f1d2e70a98a9438c6bdd8beb7f5adfb0a86a4f179a925528472da8af5d859d88309c61cbdd2aa94baa029636f4cc0870c813
|
data/lib/aikido/zen/agent.rb
CHANGED
|
@@ -21,15 +21,20 @@ module Aikido::Zen
|
|
|
21
21
|
collector: Aikido::Zen.collector,
|
|
22
22
|
detached_agent: Aikido::Zen.detached_agent,
|
|
23
23
|
worker: Aikido::Zen::Worker.new(config: config),
|
|
24
|
-
api_client: Aikido::Zen::APIClient.new(config: config)
|
|
24
|
+
api_client: Aikido::Zen::APIClient.new(config: config),
|
|
25
|
+
api_stream: Aikido::Zen::APIStream.new(config: config)
|
|
25
26
|
)
|
|
26
|
-
@started_at = nil
|
|
27
|
-
|
|
28
27
|
@config = config
|
|
29
|
-
@worker = worker
|
|
30
|
-
@api_client = api_client
|
|
31
28
|
@collector = collector
|
|
32
29
|
@detached_agent = detached_agent
|
|
30
|
+
@worker = worker
|
|
31
|
+
@api_client = api_client
|
|
32
|
+
@api_stream = api_stream
|
|
33
|
+
|
|
34
|
+
@started_at = nil
|
|
35
|
+
|
|
36
|
+
@runtime_config_update_mutex = Mutex.new
|
|
37
|
+
@runtime_firewall_lists_update_mutex = Mutex.new
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
def started?
|
|
@@ -59,7 +64,7 @@ module Aikido::Zen
|
|
|
59
64
|
at_exit { stop! if started? }
|
|
60
65
|
|
|
61
66
|
report(Events::Started.new(time: @started_at)) do |response|
|
|
62
|
-
if
|
|
67
|
+
if update_settings_from_runtime_config!(response)
|
|
63
68
|
updated_settings!
|
|
64
69
|
@config.logger.info("Updated runtime settings")
|
|
65
70
|
end
|
|
@@ -68,12 +73,17 @@ module Aikido::Zen
|
|
|
68
73
|
end
|
|
69
74
|
|
|
70
75
|
begin
|
|
71
|
-
|
|
76
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
72
77
|
@config.logger.info("Updated runtime firewall list")
|
|
73
78
|
rescue => err
|
|
74
79
|
@config.logger.error(err.message)
|
|
75
80
|
end
|
|
76
81
|
|
|
82
|
+
if @config.realtime_settings_updates_enabled?
|
|
83
|
+
@api_stream.handle("config-updated") { |event| settings_updated(event) }
|
|
84
|
+
@api_stream.start!
|
|
85
|
+
end
|
|
86
|
+
|
|
77
87
|
poll_for_setting_updates
|
|
78
88
|
|
|
79
89
|
@config.initial_heartbeat_delays.each do |heartbeat_delay|
|
|
@@ -92,6 +102,8 @@ module Aikido::Zen
|
|
|
92
102
|
@config.logger.info("Stopping Aikido agent")
|
|
93
103
|
@started_at = nil
|
|
94
104
|
@worker.shutdown
|
|
105
|
+
|
|
106
|
+
@api_stream.stop!
|
|
95
107
|
end
|
|
96
108
|
|
|
97
109
|
# Respond to the runtime settings changing after being fetched from the
|
|
@@ -157,11 +169,11 @@ module Aikido::Zen
|
|
|
157
169
|
|
|
158
170
|
heartbeat = @collector.flush
|
|
159
171
|
report(heartbeat) do |response|
|
|
160
|
-
if
|
|
172
|
+
if update_settings_from_runtime_config!(response)
|
|
161
173
|
updated_settings!
|
|
162
174
|
@config.logger.info("Updated runtime settings after heartbeat")
|
|
163
175
|
|
|
164
|
-
|
|
176
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
165
177
|
@config.logger.info("Updated runtime firewall list after heartbeat")
|
|
166
178
|
end
|
|
167
179
|
end
|
|
@@ -177,23 +189,68 @@ module Aikido::Zen
|
|
|
177
189
|
def poll_for_setting_updates
|
|
178
190
|
@worker.every(@config.polling_interval) do
|
|
179
191
|
if @api_client.should_fetch_settings?
|
|
180
|
-
if
|
|
192
|
+
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config)
|
|
181
193
|
updated_settings!
|
|
182
194
|
@config.logger.info("Updated runtime settings after polling")
|
|
183
195
|
end
|
|
184
196
|
|
|
185
|
-
|
|
197
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
186
198
|
@config.logger.info("Updated runtime firewall list after polling")
|
|
187
199
|
end
|
|
188
200
|
end
|
|
189
201
|
end
|
|
190
202
|
|
|
191
|
-
private
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
def settings_updated(event)
|
|
206
|
+
updated_at = Time.at(event[:data]["configUpdatedAt"].to_i)
|
|
207
|
+
|
|
208
|
+
if should_fetch_settings?(updated_at)
|
|
209
|
+
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config)
|
|
210
|
+
updated_settings!
|
|
211
|
+
@config.logger.info("Updated runtime settings after server-side event")
|
|
212
|
+
|
|
213
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
214
|
+
@config.logger.info("Updated runtime firewall list after server-side event")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def should_fetch_settings?(updated_at, last_updated_at = Aikido::Zen.runtime_settings.updated_at)
|
|
220
|
+
return false unless @api_client.can_make_requests?
|
|
221
|
+
return true if last_updated_at.nil?
|
|
222
|
+
|
|
223
|
+
updated_at > last_updated_at
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def heartbeats
|
|
192
227
|
@heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
|
|
193
228
|
config: @config,
|
|
194
229
|
worker: @worker
|
|
195
230
|
)
|
|
196
231
|
end
|
|
232
|
+
|
|
233
|
+
# @param data [Hash]
|
|
234
|
+
# @return [Boolean]
|
|
235
|
+
def update_settings_from_runtime_config!(data)
|
|
236
|
+
return unless @runtime_config_update_mutex.try_lock
|
|
237
|
+
begin
|
|
238
|
+
Aikido::Zen.runtime_settings.update_from_runtime_config_json(data)
|
|
239
|
+
ensure
|
|
240
|
+
@runtime_config_update_mutex.unlock
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# @param data [Hash]
|
|
245
|
+
# @return [Boolean]
|
|
246
|
+
def update_settings_from_runtime_firewall_lists!(data)
|
|
247
|
+
return unless @runtime_firewall_lists_update_mutex.try_lock
|
|
248
|
+
begin
|
|
249
|
+
Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(data)
|
|
250
|
+
ensure
|
|
251
|
+
@runtime_firewall_lists_update_mutex.unlock
|
|
252
|
+
end
|
|
253
|
+
end
|
|
197
254
|
end
|
|
198
255
|
end
|
|
199
256
|
|
|
@@ -16,6 +16,12 @@ module Aikido::Zen
|
|
|
16
16
|
@config = config
|
|
17
17
|
@system_info = system_info
|
|
18
18
|
@rate_limiter = rate_limiter
|
|
19
|
+
|
|
20
|
+
@should_fetch_settings_endpoint = @config.realtime_endpoint
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def should_fetch_settings_endpoint=(uri)
|
|
24
|
+
@should_fetch_settings_endpoint = URI(uri)
|
|
19
25
|
end
|
|
20
26
|
|
|
21
27
|
# @return [Boolean] whether we have a configured token.
|
|
@@ -38,10 +44,10 @@ module Aikido::Zen
|
|
|
38
44
|
|
|
39
45
|
response = request(
|
|
40
46
|
Net::HTTP::Get.new("/config", default_headers),
|
|
41
|
-
base_url: @
|
|
47
|
+
base_url: @should_fetch_settings_endpoint
|
|
42
48
|
)
|
|
43
49
|
|
|
44
|
-
new_updated_at = Time.at(response["configUpdatedAt"].to_i
|
|
50
|
+
new_updated_at = Time.at(response["configUpdatedAt"].to_i)
|
|
45
51
|
new_updated_at > last_updated_at
|
|
46
52
|
end
|
|
47
53
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Aikido::Zen
|
|
8
|
+
class APIStream
|
|
9
|
+
def initialize(
|
|
10
|
+
config: Aikido::Zen.config,
|
|
11
|
+
min_backoff: 5,
|
|
12
|
+
max_backoff: 60,
|
|
13
|
+
backoff_reset: 30,
|
|
14
|
+
open_timeout: 5,
|
|
15
|
+
write_timeout: open_timeout,
|
|
16
|
+
read_timeout: 70
|
|
17
|
+
)
|
|
18
|
+
@config = config
|
|
19
|
+
@min_backoff = min_backoff
|
|
20
|
+
@max_backoff = max_backoff
|
|
21
|
+
@backoff_reset = backoff_reset
|
|
22
|
+
@open_timeout = open_timeout
|
|
23
|
+
@write_timeout = write_timeout
|
|
24
|
+
@read_timeout = read_timeout
|
|
25
|
+
|
|
26
|
+
@running = Concurrent::AtomicBoolean.new
|
|
27
|
+
@thread = nil
|
|
28
|
+
@http = nil
|
|
29
|
+
|
|
30
|
+
endpoint = @config.realtime_endpoint
|
|
31
|
+
|
|
32
|
+
@host = endpoint.host
|
|
33
|
+
@port = endpoint.port
|
|
34
|
+
@use_ssl = endpoint.scheme == "https"
|
|
35
|
+
@token = @config.api_token
|
|
36
|
+
|
|
37
|
+
@handlers = Concurrent::Array.new
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def running?
|
|
41
|
+
@running.true?
|
|
42
|
+
end
|
|
43
|
+
alias_method :started?, :running?
|
|
44
|
+
|
|
45
|
+
def start!
|
|
46
|
+
return false unless @running.make_true
|
|
47
|
+
|
|
48
|
+
@thread = Thread.new do
|
|
49
|
+
backoff = @min_backoff
|
|
50
|
+
|
|
51
|
+
while running?
|
|
52
|
+
time_before = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
work
|
|
56
|
+
rescue IOError => err
|
|
57
|
+
@config.logger.debug("Error in API stream: #{err.class}: #{err.message}") if running?
|
|
58
|
+
rescue Timeout::Error, SocketError, SystemCallError, OpenSSL::OpenSSLError => err
|
|
59
|
+
@config.logger.debug("Error in API stream: #{err.class}: #{err.message}")
|
|
60
|
+
rescue => err
|
|
61
|
+
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
break unless running?
|
|
65
|
+
|
|
66
|
+
time_after = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
|
67
|
+
|
|
68
|
+
backoff = if time_after - time_before > @backoff_reset
|
|
69
|
+
@min_backoff
|
|
70
|
+
else
|
|
71
|
+
[backoff * 2, @max_backoff].min
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
jitter = rand * backoff / 2
|
|
75
|
+
|
|
76
|
+
@config.logger.debug("API stream reconnecting in %d seconds" % (backoff + jitter).ceil)
|
|
77
|
+
|
|
78
|
+
sleep(backoff + jitter)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def stop!
|
|
86
|
+
return false unless @running.make_false
|
|
87
|
+
|
|
88
|
+
begin
|
|
89
|
+
@http&.finish
|
|
90
|
+
rescue IOError
|
|
91
|
+
# ignore error
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
begin
|
|
95
|
+
@thread&.wakeup
|
|
96
|
+
rescue ThreadError
|
|
97
|
+
# ignore error
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@thread.join(@read_timeout)
|
|
101
|
+
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def handle(type, &block)
|
|
106
|
+
raise ArgumentError, "block required" unless block
|
|
107
|
+
|
|
108
|
+
@handlers << proc do |event|
|
|
109
|
+
block.call(event) if type === event[:type]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private def work
|
|
114
|
+
http = Net::HTTP.new(@host, @port)
|
|
115
|
+
http.use_ssl = @use_ssl
|
|
116
|
+
http.open_timeout = @open_timeout
|
|
117
|
+
http.write_timeout = @write_timeout
|
|
118
|
+
http.read_timeout = @read_timeout
|
|
119
|
+
http.max_retries = 0
|
|
120
|
+
|
|
121
|
+
request = Net::HTTP::Get.new("/api/runtime/stream")
|
|
122
|
+
request["Authorization"] = @token
|
|
123
|
+
request["Accept"] = "text/event-stream"
|
|
124
|
+
request["Cache-Control"] = "no-cache"
|
|
125
|
+
|
|
126
|
+
@config.logger.debug("API stream connecting")
|
|
127
|
+
http.start
|
|
128
|
+
@config.logger.debug("API stream connected")
|
|
129
|
+
|
|
130
|
+
@http = http
|
|
131
|
+
|
|
132
|
+
begin
|
|
133
|
+
http.request(request) do |response|
|
|
134
|
+
case response.code.to_i
|
|
135
|
+
when 200
|
|
136
|
+
# empty
|
|
137
|
+
when 401, 403
|
|
138
|
+
@running.make_false
|
|
139
|
+
return nil
|
|
140
|
+
else
|
|
141
|
+
return nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
buffer = +""
|
|
145
|
+
|
|
146
|
+
response.read_body do |chunk|
|
|
147
|
+
return nil unless running?
|
|
148
|
+
|
|
149
|
+
@config.logger.debug("API stream received chunk of #{chunk.bytesize} bytes")
|
|
150
|
+
|
|
151
|
+
buffer << chunk
|
|
152
|
+
|
|
153
|
+
while (index = buffer.index("\n\n"))
|
|
154
|
+
event_str = buffer.slice!(0..index + 1)
|
|
155
|
+
buffer = buffer.lstrip
|
|
156
|
+
|
|
157
|
+
event = {}
|
|
158
|
+
|
|
159
|
+
begin
|
|
160
|
+
event_str.each_line do |line|
|
|
161
|
+
case line
|
|
162
|
+
when /^event:\s*(.+)/
|
|
163
|
+
event[:type] = $1.strip
|
|
164
|
+
when /^data:\s*(.+)/
|
|
165
|
+
event[:data] = JSON.parse($1.strip)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
rescue => err
|
|
169
|
+
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
|
|
170
|
+
next
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
@handlers.each do |handler|
|
|
174
|
+
handler.call(event)
|
|
175
|
+
rescue => err
|
|
176
|
+
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
ensure
|
|
182
|
+
@config.logger.debug("API stream disconnecting")
|
|
183
|
+
|
|
184
|
+
@http = nil
|
|
185
|
+
|
|
186
|
+
http.finish if http.started?
|
|
187
|
+
|
|
188
|
+
@config.logger.debug("API stream disconnected")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
module Aikido::Zen
|
|
4
6
|
module AttackWave
|
|
5
7
|
module Helpers
|
|
6
|
-
def self.web_scanner?(context)
|
|
7
|
-
return true if suspicious_request?(context)
|
|
8
|
+
def self.web_scanner?(context, status_code)
|
|
9
|
+
return true if suspicious_request?(context, status_code)
|
|
8
10
|
|
|
9
11
|
return true if include_suspicious_payload?(context)
|
|
10
12
|
|
|
11
13
|
false
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
def self.suspicious_request?(context)
|
|
16
|
+
def self.suspicious_request?(context, status_code)
|
|
15
17
|
request = context.request
|
|
16
18
|
|
|
17
|
-
suspicious_method?(request.request_method) || suspicious_path?(request.path_info)
|
|
19
|
+
suspicious_method?(request.request_method) || suspicious_path?(request.path_info, status_code)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def self.suspicious_method?(method)
|
|
21
23
|
SUSPICIOUS_METHODS.include?(method.downcase)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def self.suspicious_path?(path)
|
|
26
|
+
def self.suspicious_path?(path, status_code)
|
|
25
27
|
path_parts = path.downcase.split("/")
|
|
26
28
|
|
|
27
29
|
file_name = path_parts.pop if path_parts.length > 0
|
|
@@ -34,6 +36,8 @@ module Aikido::Zen
|
|
|
34
36
|
file_extension = file_name_parts.pop if file_name_parts.length > 1
|
|
35
37
|
|
|
36
38
|
return true if SUSPICIOUS_FILE_EXTENSIONS.include?(file_extension)
|
|
39
|
+
|
|
40
|
+
return true if FOREIGN_EXTENSIONS.include?(file_extension) && status_code == 404
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
path_parts.any? do |directory_name|
|
|
@@ -434,6 +438,11 @@ module Aikido::Zen
|
|
|
434
438
|
"sqlite3db"
|
|
435
439
|
].map(&:downcase).freeze
|
|
436
440
|
|
|
441
|
+
# Extensions that a Ruby app would not natively serve. Requests to these
|
|
442
|
+
# paths are only treated as scan hits when the response is 404 — a 200
|
|
443
|
+
# may indicate the app is proxying to a PHP/Java backend.
|
|
444
|
+
FOREIGN_EXTENSIONS = Set.new(%w[php php3 php4 php5 phtml java jsp jspx]).freeze
|
|
445
|
+
|
|
437
446
|
SUSPICIOUS_SQL_KEYWORDS = [
|
|
438
447
|
"SELECT (CASE WHEN",
|
|
439
448
|
"SELECT COUNT(",
|
|
@@ -21,14 +21,14 @@ module Aikido::Zen
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def attack_wave?(context)
|
|
24
|
+
def attack_wave?(context, status_code = nil)
|
|
25
25
|
client_ip = context.request.client_ip
|
|
26
26
|
|
|
27
27
|
return false unless client_ip
|
|
28
28
|
|
|
29
29
|
return false if @event_times[client_ip]
|
|
30
30
|
|
|
31
|
-
return false unless AttackWave::Helpers.web_scanner?(context)
|
|
31
|
+
return false unless AttackWave::Helpers.web_scanner?(context, status_code)
|
|
32
32
|
|
|
33
33
|
request_count = @request_counts[client_ip] += 1
|
|
34
34
|
|
data/lib/aikido/zen/config.rb
CHANGED
|
@@ -216,6 +216,11 @@ module Aikido::Zen
|
|
|
216
216
|
# Defaults to 1000 entries.
|
|
217
217
|
attr_accessor :idor_max_cache_entries
|
|
218
218
|
|
|
219
|
+
# @return [Boolean] whether the realtime settings updates feature is enabled.
|
|
220
|
+
# Defaults to false.
|
|
221
|
+
attr_accessor :realtime_settings_updates_enabled
|
|
222
|
+
alias_method :realtime_settings_updates_enabled?, :realtime_settings_updates_enabled
|
|
223
|
+
|
|
219
224
|
def initialize
|
|
220
225
|
self.insert_middleware_after = ::ActionDispatch::RemoteIp
|
|
221
226
|
self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
|
|
@@ -261,6 +266,7 @@ module Aikido::Zen
|
|
|
261
266
|
self.idor_tenant_column_name = nil
|
|
262
267
|
self.idor_excluded_table_names = []
|
|
263
268
|
self.idor_max_cache_entries = 1000
|
|
269
|
+
self.realtime_settings_updates_enabled = false
|
|
264
270
|
end
|
|
265
271
|
|
|
266
272
|
# Set the base URL for API requests.
|
data/lib/aikido/zen/context.rb
CHANGED
|
@@ -98,7 +98,9 @@ module Aikido::Zen
|
|
|
98
98
|
|
|
99
99
|
# @!visibility private
|
|
100
100
|
def extract_payloads_from(data, source_type, prefix = nil)
|
|
101
|
-
if data.
|
|
101
|
+
if data.is_a?(String)
|
|
102
|
+
[Payload.new(data, source_type, prefix.to_s)]
|
|
103
|
+
elsif data.respond_to?(:to_hash)
|
|
102
104
|
data.to_hash.flat_map do |key, value|
|
|
103
105
|
extract_payloads_from(value, source_type, [prefix, key].compact.join("."))
|
|
104
106
|
end
|
|
@@ -125,7 +127,7 @@ module Aikido::Zen
|
|
|
125
127
|
|
|
126
128
|
payloads
|
|
127
129
|
else
|
|
128
|
-
[
|
|
130
|
+
[]
|
|
129
131
|
end
|
|
130
132
|
end
|
|
131
133
|
|
|
@@ -12,28 +12,29 @@ module Aikido
|
|
|
12
12
|
|
|
13
13
|
def call(env)
|
|
14
14
|
response = @app.call(env)
|
|
15
|
+
status_code = response[0].to_i
|
|
15
16
|
|
|
16
17
|
context = @zen.current_context
|
|
17
|
-
protect(context)
|
|
18
|
+
protect(context, status_code)
|
|
18
19
|
|
|
19
20
|
response
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
# @api private
|
|
23
24
|
# Visible for testing.
|
|
24
|
-
def attack_wave?(context)
|
|
25
|
+
def attack_wave?(context, status_code = nil)
|
|
25
26
|
request = context.request
|
|
26
27
|
return false if request.nil?
|
|
27
28
|
|
|
28
29
|
return false if @settings.bypassed_ips.include?(request.client_ip)
|
|
29
30
|
|
|
30
|
-
@zen.attack_wave_detector.attack_wave?(context)
|
|
31
|
+
@zen.attack_wave_detector.attack_wave?(context, status_code)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
# @api private
|
|
34
35
|
# Visible for testing.
|
|
35
|
-
def protect(context)
|
|
36
|
-
if attack_wave?(context)
|
|
36
|
+
def protect(context, status_code = nil)
|
|
37
|
+
if attack_wave?(context, status_code)
|
|
37
38
|
client_ip = context.request.client_ip
|
|
38
39
|
|
|
39
40
|
request = Aikido::Zen::AttackWave::Request.new(
|
|
@@ -80,11 +80,11 @@ module Aikido::Zen
|
|
|
80
80
|
#
|
|
81
81
|
# @param data [Hash] the decoded JSON payload from the /api/runtime/config
|
|
82
82
|
# API endpoint.
|
|
83
|
-
# @return [
|
|
83
|
+
# @return [Boolean]
|
|
84
84
|
def update_from_runtime_config_json(data)
|
|
85
85
|
last_updated_at = updated_at
|
|
86
86
|
|
|
87
|
-
self.updated_at = Time.at(data["configUpdatedAt"].to_i
|
|
87
|
+
self.updated_at = Time.at(data["configUpdatedAt"].to_i)
|
|
88
88
|
self.heartbeat_interval = data["heartbeatIntervalInMS"].to_i / 1000
|
|
89
89
|
self.endpoints = RuntimeSettings::Endpoints.from_json(data["endpoints"])
|
|
90
90
|
self.blocked_user_ids = data["blockedUserIds"]
|
|
@@ -105,7 +105,7 @@ module Aikido::Zen
|
|
|
105
105
|
#
|
|
106
106
|
# @param data [Hash] the decoded JSON payload from the /api/runtime/firewall/lists
|
|
107
107
|
# API endpoint.
|
|
108
|
-
# @return [
|
|
108
|
+
# @return [Boolean]
|
|
109
109
|
def update_from_runtime_firewall_lists_json(data)
|
|
110
110
|
self.blocked_user_agent_regexp = pattern(data["blockedUserAgents"])
|
|
111
111
|
|
|
@@ -142,6 +142,8 @@ module Aikido::Zen
|
|
|
142
142
|
data["monitoredIPAddresses"]&.each do |ip_list|
|
|
143
143
|
monitored_ip_lists << RuntimeSettings::IPList.from_json(ip_list)
|
|
144
144
|
end
|
|
145
|
+
|
|
146
|
+
true
|
|
145
147
|
end
|
|
146
148
|
|
|
147
149
|
# Construct a regular expression from the non-nil and non-empty string,
|
|
@@ -45,6 +45,9 @@ module Aikido::Zen
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def attack?
|
|
48
|
+
# We block home-relative path
|
|
49
|
+
return true if @input.start_with?("~") && @filepath.start_with?(@input)
|
|
50
|
+
|
|
48
51
|
# Single character are ignored because they don't pose a big threat
|
|
49
52
|
return false if @input.length <= 1
|
|
50
53
|
|
|
@@ -36,7 +36,7 @@ module Aikido::Zen
|
|
|
36
36
|
def attack?
|
|
37
37
|
return unless @config.stored_ssrf? # Feature flag
|
|
38
38
|
|
|
39
|
-
return if @config.imds_allowed_hosts.include?(@hostname)
|
|
39
|
+
return if @config.imds_allowed_hosts.include?(@hostname.chomp("."))
|
|
40
40
|
|
|
41
41
|
@addresses.find do |address|
|
|
42
42
|
DANGEROUS_ADDRESSES.any? do |dangerous_address|
|
data/lib/aikido/zen/version.rb
CHANGED
data/lib/aikido/zen.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "zen/system_info"
|
|
|
10
10
|
require_relative "zen/worker"
|
|
11
11
|
require_relative "zen/agent"
|
|
12
12
|
require_relative "zen/api_client"
|
|
13
|
+
require_relative "zen/api_stream"
|
|
13
14
|
require_relative "zen/context"
|
|
14
15
|
require_relative "zen/current_context"
|
|
15
16
|
require_relative "zen/detached_agent"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aikido-zen
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aikido Security
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -98,6 +98,7 @@ files:
|
|
|
98
98
|
- lib/aikido/zen/agent.rb
|
|
99
99
|
- lib/aikido/zen/agent/heartbeats_manager.rb
|
|
100
100
|
- lib/aikido/zen/api_client.rb
|
|
101
|
+
- lib/aikido/zen/api_stream.rb
|
|
101
102
|
- lib/aikido/zen/attack.rb
|
|
102
103
|
- lib/aikido/zen/attack_wave.rb
|
|
103
104
|
- lib/aikido/zen/attack_wave/helpers.rb
|