aikido-zen 1.4.1-aarch64-linux → 1.5.0-aarch64-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/lib/aikido/zen/agent.rb +77 -12
- data/lib/aikido/zen/api_client.rb +8 -2
- data/lib/aikido/zen/api_stream.rb +217 -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 +21 -0
- 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: fcc4f05fee5eb4c2b6bc4b75313c8d1a8948282b2e1ea3e4ef0e735055e55f42
|
|
4
|
+
data.tar.gz: 692ace187aae4b18bce5d1aefaa10c7aeacbfd28016645792556ae0aafe2c8dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a32eea6aec3687a23e54c6cc3e1365b4e4bd9e2cdcf93d39acf46289ad6063ca0671061ed1aada13ab18267459af66d0dc5660f08745c0f290f705086cdb214e
|
|
7
|
+
data.tar.gz: a145a0fcd6e137f4ca276219e7a235811da1bb3aed09ea7e0a123ab7e69ae09b5df657a0050de99e30165040906db9b948979a24e0a736a61e992caad33fed53
|
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,25 @@ 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
|
+
if @api_stream.can_connect?
|
|
84
|
+
@api_stream.handle("config-updated") { |event| settings_updated(event) }
|
|
85
|
+
@api_stream.start!
|
|
86
|
+
|
|
87
|
+
# Use the realtime setting updates endpoint when polling to check
|
|
88
|
+
# whether settings should be fetched.
|
|
89
|
+
@api_client.should_fetch_settings_endpoint = @config.realtime_settings_updates_endpoint
|
|
90
|
+
else
|
|
91
|
+
@config.logger.warn("Can't reach #{Aikido::Zen.config.realtime_settings_updates_endpoint}, make sure it's in your outbound firewall allowlist. Realtime config updates won't be available, switched to polling.")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
77
95
|
poll_for_setting_updates
|
|
78
96
|
|
|
79
97
|
@config.initial_heartbeat_delays.each do |heartbeat_delay|
|
|
@@ -92,6 +110,8 @@ module Aikido::Zen
|
|
|
92
110
|
@config.logger.info("Stopping Aikido agent")
|
|
93
111
|
@started_at = nil
|
|
94
112
|
@worker.shutdown
|
|
113
|
+
|
|
114
|
+
@api_stream.stop!
|
|
95
115
|
end
|
|
96
116
|
|
|
97
117
|
# Respond to the runtime settings changing after being fetched from the
|
|
@@ -157,11 +177,11 @@ module Aikido::Zen
|
|
|
157
177
|
|
|
158
178
|
heartbeat = @collector.flush
|
|
159
179
|
report(heartbeat) do |response|
|
|
160
|
-
if
|
|
180
|
+
if update_settings_from_runtime_config!(response)
|
|
161
181
|
updated_settings!
|
|
162
182
|
@config.logger.info("Updated runtime settings after heartbeat")
|
|
163
183
|
|
|
164
|
-
|
|
184
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
165
185
|
@config.logger.info("Updated runtime firewall list after heartbeat")
|
|
166
186
|
end
|
|
167
187
|
end
|
|
@@ -177,23 +197,68 @@ module Aikido::Zen
|
|
|
177
197
|
def poll_for_setting_updates
|
|
178
198
|
@worker.every(@config.polling_interval) do
|
|
179
199
|
if @api_client.should_fetch_settings?
|
|
180
|
-
if
|
|
200
|
+
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config)
|
|
181
201
|
updated_settings!
|
|
182
202
|
@config.logger.info("Updated runtime settings after polling")
|
|
183
203
|
end
|
|
184
204
|
|
|
185
|
-
|
|
205
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
186
206
|
@config.logger.info("Updated runtime firewall list after polling")
|
|
187
207
|
end
|
|
188
208
|
end
|
|
189
209
|
end
|
|
190
210
|
|
|
191
|
-
private
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
def settings_updated(event)
|
|
214
|
+
updated_at = Time.at(event[:data]["configUpdatedAt"].to_i)
|
|
215
|
+
|
|
216
|
+
if should_fetch_settings?(updated_at)
|
|
217
|
+
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config)
|
|
218
|
+
updated_settings!
|
|
219
|
+
@config.logger.info("Updated runtime settings after server-side event")
|
|
220
|
+
|
|
221
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
222
|
+
@config.logger.info("Updated runtime firewall list after server-side event")
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def should_fetch_settings?(updated_at, last_updated_at = Aikido::Zen.runtime_settings.updated_at)
|
|
228
|
+
return false unless @api_client.can_make_requests?
|
|
229
|
+
return true if last_updated_at.nil?
|
|
230
|
+
|
|
231
|
+
updated_at > last_updated_at
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def heartbeats
|
|
192
235
|
@heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
|
|
193
236
|
config: @config,
|
|
194
237
|
worker: @worker
|
|
195
238
|
)
|
|
196
239
|
end
|
|
240
|
+
|
|
241
|
+
# @param data [Hash]
|
|
242
|
+
# @return [Boolean]
|
|
243
|
+
def update_settings_from_runtime_config!(data)
|
|
244
|
+
return unless @runtime_config_update_mutex.try_lock
|
|
245
|
+
begin
|
|
246
|
+
Aikido::Zen.runtime_settings.update_from_runtime_config_json(data)
|
|
247
|
+
ensure
|
|
248
|
+
@runtime_config_update_mutex.unlock
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# @param data [Hash]
|
|
253
|
+
# @return [Boolean]
|
|
254
|
+
def update_settings_from_runtime_firewall_lists!(data)
|
|
255
|
+
return unless @runtime_firewall_lists_update_mutex.try_lock
|
|
256
|
+
begin
|
|
257
|
+
Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(data)
|
|
258
|
+
ensure
|
|
259
|
+
@runtime_firewall_lists_update_mutex.unlock
|
|
260
|
+
end
|
|
261
|
+
end
|
|
197
262
|
end
|
|
198
263
|
end
|
|
199
264
|
|
|
@@ -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,217 @@
|
|
|
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_settings_updates_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
|
+
# @return [Boolean] whether we could connect to the realtime endpoint
|
|
41
|
+
def can_connect?
|
|
42
|
+
http = Net::HTTP.new(@host, @port)
|
|
43
|
+
http.use_ssl = @use_ssl
|
|
44
|
+
http.open_timeout = 5
|
|
45
|
+
http.write_timeout = 5
|
|
46
|
+
http.read_timeout = 5
|
|
47
|
+
http.max_retries = 0
|
|
48
|
+
|
|
49
|
+
request = Net::HTTP::Get.new("/config")
|
|
50
|
+
request["Authorization"] = @token
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
http.request(request)
|
|
54
|
+
|
|
55
|
+
return true
|
|
56
|
+
rescue Timeout::Error, SocketError, IOError, SystemCallError, OpenSSL::OpenSSLError => err
|
|
57
|
+
@config.logger.debug("Error probing realtime endpoint: #{err.class}: #{err.message}")
|
|
58
|
+
rescue => err
|
|
59
|
+
@config.logger.error("Error probing realtime endpoint: #{err.class}: #{err.message}")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def running?
|
|
66
|
+
@running.true?
|
|
67
|
+
end
|
|
68
|
+
alias_method :started?, :running?
|
|
69
|
+
|
|
70
|
+
def start!
|
|
71
|
+
return false unless @running.make_true
|
|
72
|
+
|
|
73
|
+
@thread = Thread.new do
|
|
74
|
+
backoff = @min_backoff
|
|
75
|
+
|
|
76
|
+
while running?
|
|
77
|
+
time_before = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
|
78
|
+
|
|
79
|
+
begin
|
|
80
|
+
work
|
|
81
|
+
rescue IOError => err
|
|
82
|
+
@config.logger.debug("Error in API stream: #{err.class}: #{err.message}") if running?
|
|
83
|
+
rescue Timeout::Error, SocketError, SystemCallError, OpenSSL::OpenSSLError => err
|
|
84
|
+
@config.logger.debug("Error in API stream: #{err.class}: #{err.message}")
|
|
85
|
+
rescue => err
|
|
86
|
+
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
break unless running?
|
|
90
|
+
|
|
91
|
+
time_after = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
|
92
|
+
|
|
93
|
+
backoff = if time_after - time_before > @backoff_reset
|
|
94
|
+
@min_backoff
|
|
95
|
+
else
|
|
96
|
+
[backoff * 2, @max_backoff].min
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
jitter = rand * backoff / 2
|
|
100
|
+
|
|
101
|
+
@config.logger.debug("API stream reconnecting in %d seconds" % (backoff + jitter).ceil)
|
|
102
|
+
|
|
103
|
+
sleep(backoff + jitter)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def stop!
|
|
111
|
+
return false unless @running.make_false
|
|
112
|
+
|
|
113
|
+
begin
|
|
114
|
+
@http&.finish
|
|
115
|
+
rescue IOError
|
|
116
|
+
# ignore error
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
begin
|
|
120
|
+
@thread&.wakeup
|
|
121
|
+
rescue ThreadError
|
|
122
|
+
# ignore error
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
@thread.join(@read_timeout)
|
|
126
|
+
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def handle(type, &block)
|
|
131
|
+
raise ArgumentError, "block required" unless block
|
|
132
|
+
|
|
133
|
+
@handlers << proc do |event|
|
|
134
|
+
block.call(event) if type === event[:type]
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private def work
|
|
139
|
+
http = Net::HTTP.new(@host, @port)
|
|
140
|
+
http.use_ssl = @use_ssl
|
|
141
|
+
http.open_timeout = @open_timeout
|
|
142
|
+
http.write_timeout = @write_timeout
|
|
143
|
+
http.read_timeout = @read_timeout
|
|
144
|
+
http.max_retries = 0
|
|
145
|
+
|
|
146
|
+
request = Net::HTTP::Get.new("/api/runtime/stream")
|
|
147
|
+
request["Authorization"] = @token
|
|
148
|
+
request["Accept"] = "text/event-stream"
|
|
149
|
+
request["Cache-Control"] = "no-cache"
|
|
150
|
+
|
|
151
|
+
@config.logger.debug("API stream connecting")
|
|
152
|
+
http.start
|
|
153
|
+
@config.logger.debug("API stream connected")
|
|
154
|
+
|
|
155
|
+
@http = http
|
|
156
|
+
|
|
157
|
+
begin
|
|
158
|
+
http.request(request) do |response|
|
|
159
|
+
case response.code.to_i
|
|
160
|
+
when 200
|
|
161
|
+
# empty
|
|
162
|
+
when 401, 403
|
|
163
|
+
@running.make_false
|
|
164
|
+
return nil
|
|
165
|
+
else
|
|
166
|
+
return nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
buffer = +""
|
|
170
|
+
|
|
171
|
+
response.read_body do |chunk|
|
|
172
|
+
return nil unless running?
|
|
173
|
+
|
|
174
|
+
@config.logger.debug("API stream received chunk of #{chunk.bytesize} bytes")
|
|
175
|
+
|
|
176
|
+
buffer << chunk
|
|
177
|
+
|
|
178
|
+
while (index = buffer.index("\n\n"))
|
|
179
|
+
event_str = buffer.slice!(0..index + 1)
|
|
180
|
+
buffer = buffer.lstrip
|
|
181
|
+
|
|
182
|
+
event = {}
|
|
183
|
+
|
|
184
|
+
begin
|
|
185
|
+
event_str.each_line do |line|
|
|
186
|
+
case line
|
|
187
|
+
when /^event:\s*(.+)/
|
|
188
|
+
event[:type] = $1.strip
|
|
189
|
+
when /^data:\s*(.+)/
|
|
190
|
+
event[:data] = JSON.parse($1.strip)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
rescue => err
|
|
194
|
+
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
|
|
195
|
+
next
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
@handlers.each do |handler|
|
|
199
|
+
handler.call(event)
|
|
200
|
+
rescue => err
|
|
201
|
+
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
ensure
|
|
207
|
+
@config.logger.debug("API stream disconnecting")
|
|
208
|
+
|
|
209
|
+
@http = nil
|
|
210
|
+
|
|
211
|
+
http.finish if http.started?
|
|
212
|
+
|
|
213
|
+
@config.logger.debug("API stream disconnected")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
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,15 @@ 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
|
+
|
|
224
|
+
# @return [URI] The HTTP host for realtime settings updates.
|
|
225
|
+
# Defaults to +https://zen.aikido.dev+.
|
|
226
|
+
attr_reader :realtime_settings_updates_endpoint
|
|
227
|
+
|
|
219
228
|
def initialize
|
|
220
229
|
self.insert_middleware_after = ::ActionDispatch::RemoteIp
|
|
221
230
|
self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
|
|
@@ -261,6 +270,8 @@ module Aikido::Zen
|
|
|
261
270
|
self.idor_tenant_column_name = nil
|
|
262
271
|
self.idor_excluded_table_names = []
|
|
263
272
|
self.idor_max_cache_entries = 1000
|
|
273
|
+
self.realtime_settings_updates_enabled = false
|
|
274
|
+
self.realtime_settings_updates_endpoint = ENV.fetch("AIKIDO_REALTIME_SETTINGS_UPDATES_ENDPOINT", DEFAULT_REALTIME_SETTINGS_UPDATES_BASE_URL)
|
|
264
275
|
end
|
|
265
276
|
|
|
266
277
|
# Set the base URL for API requests.
|
|
@@ -277,6 +288,13 @@ module Aikido::Zen
|
|
|
277
288
|
@realtime_endpoint = URI(url)
|
|
278
289
|
end
|
|
279
290
|
|
|
291
|
+
# Set the base URL for the realtime settings updates feature.
|
|
292
|
+
#
|
|
293
|
+
# @param url [String, URI]
|
|
294
|
+
def realtime_settings_updates_endpoint=(url)
|
|
295
|
+
@realtime_settings_updates_endpoint = URI(url)
|
|
296
|
+
end
|
|
297
|
+
|
|
280
298
|
# Set the logger and configure its severity level according to agent's debug mode
|
|
281
299
|
# @param logger [::Logger]
|
|
282
300
|
def logger=(logger)
|
|
@@ -346,6 +364,9 @@ module Aikido::Zen
|
|
|
346
364
|
# @!visibility private
|
|
347
365
|
DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
|
|
348
366
|
|
|
367
|
+
# @!visibility private
|
|
368
|
+
DEFAULT_REALTIME_SETTINGS_UPDATES_BASE_URL = "https://zen.aikido.dev"
|
|
369
|
+
|
|
349
370
|
# @!visibility private
|
|
350
371
|
DEFAULT_JSON_ENCODER = JSON.method(:dump)
|
|
351
372
|
|
|
@@ -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.0
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Aikido Security
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -102,6 +102,7 @@ files:
|
|
|
102
102
|
- lib/aikido/zen/agent.rb
|
|
103
103
|
- lib/aikido/zen/agent/heartbeats_manager.rb
|
|
104
104
|
- lib/aikido/zen/api_client.rb
|
|
105
|
+
- lib/aikido/zen/api_stream.rb
|
|
105
106
|
- lib/aikido/zen/attack.rb
|
|
106
107
|
- lib/aikido/zen/attack_wave.rb
|
|
107
108
|
- lib/aikido/zen/attack_wave/helpers.rb
|