aikido-zen 1.5.0-aarch64-linux → 1.6.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/benchmarks/ipc/benchmark.rb +149 -0
- data/benchmarks/rpc/benchmark.rb +57 -0
- data/docs/rails.md +22 -0
- data/lib/aikido/zen/actor.rb +10 -10
- data/lib/aikido/zen/agent.rb +61 -33
- data/lib/aikido/zen/api_cache.rb +61 -0
- data/lib/aikido/zen/api_stream.rb +7 -26
- data/lib/aikido/zen/attack.rb +14 -14
- data/lib/aikido/zen/attack_wave.rb +8 -8
- data/lib/aikido/zen/collector/event.rb +26 -26
- data/lib/aikido/zen/collector/routes.rb +4 -4
- data/lib/aikido/zen/collector/sink_stats.rb +10 -10
- data/lib/aikido/zen/collector/stats.rb +17 -17
- data/lib/aikido/zen/config.rb +49 -39
- data/lib/aikido/zen/context.rb +4 -2
- data/lib/aikido/zen/errors.rb +0 -8
- data/lib/aikido/zen/event.rb +12 -12
- data/lib/aikido/zen/ipc/ipc.rb +506 -0
- data/lib/aikido/zen/ipc/rpc.rb +349 -0
- data/lib/aikido/zen/ipc.rb +4 -0
- data/lib/aikido/zen/middleware/fork_detector.rb +8 -2
- data/lib/aikido/zen/middleware/rack_throttler.rb +2 -4
- data/lib/aikido/zen/outbound_connection.rb +3 -3
- data/lib/aikido/zen/payload.rb +3 -3
- data/lib/aikido/zen/rate_limiter/result.rb +20 -0
- data/lib/aikido/zen/request.rb +14 -9
- data/lib/aikido/zen/route.rb +3 -3
- data/lib/aikido/zen/sinks/action_controller.rb +2 -4
- data/lib/aikido/zen/system_info.rb +13 -13
- data/lib/aikido/zen/version.rb +1 -1
- data/lib/aikido/zen/worker_process/agent/client.rb +141 -0
- data/lib/aikido/zen/worker_process/agent/server.rb +97 -0
- data/lib/aikido/zen/worker_process/agent.rb +4 -0
- data/lib/aikido/zen/worker_process.rb +3 -0
- data/lib/aikido/zen.rb +55 -29
- metadata +12 -6
- data/lib/aikido/zen/detached_agent/agent.rb +0 -79
- data/lib/aikido/zen/detached_agent/front_object.rb +0 -41
- data/lib/aikido/zen/detached_agent/server.rb +0 -78
- data/lib/aikido/zen/detached_agent.rb +0 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eba16aa05cb3f3506e487a3dfd7c03a1d4bbc186a397053aaf43562fad1ed4f4
|
|
4
|
+
data.tar.gz: b9815eab35c5f245eba4587c64bf4bfda422c1bcfea1d1d21eee747057723e41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c99f6276ce2d33f42d3b9ec7f5430bb647cd72cb146e583066d97d918cc0e59b21d82999dada088197627be1226579cbea675f7750c609346a2d7487f39b3e5
|
|
7
|
+
data.tar.gz: ad2e3595f4fee8181ba2083f05a2a15181cf62db472f47c112a3480dd6575846cf50d7e9c3ac420a5f3ddbd48bda9b98cff815de50a12a35e427ebbce7e68427
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "benchmark"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "aikido-zen"
|
|
6
|
+
|
|
7
|
+
class BasicIO
|
|
8
|
+
def initialize(data, timeout: 5)
|
|
9
|
+
@data = data
|
|
10
|
+
@size = data.bytesize
|
|
11
|
+
@timeout = timeout
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def read(socket)
|
|
15
|
+
buffer = String.new(encoding: Encoding::BINARY)
|
|
16
|
+
|
|
17
|
+
while buffer.bytesize < @size
|
|
18
|
+
case chunk = socket.read_nonblock(@size - buffer.bytesize, exception: false)
|
|
19
|
+
when :wait_readable
|
|
20
|
+
raise Errno::ETIMEDOUT, "read timed out" unless IO.select([socket], nil, nil, @timeout)
|
|
21
|
+
when nil
|
|
22
|
+
raise EOFError
|
|
23
|
+
else
|
|
24
|
+
buffer << chunk
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
buffer
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write(socket)
|
|
32
|
+
written = 0
|
|
33
|
+
|
|
34
|
+
while written < @data.bytesize
|
|
35
|
+
case n = socket.write_nonblock(@data.byteslice(written..), exception: false)
|
|
36
|
+
when :wait_writable
|
|
37
|
+
raise Errno::ETIMEDOUT, "write timed out" unless IO.select(nil, [socket], nil, @timeout)
|
|
38
|
+
else
|
|
39
|
+
written += n
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class TimedIO
|
|
46
|
+
include Aikido::Zen::IPC::TimedIO
|
|
47
|
+
|
|
48
|
+
def initialize(data, timeout: 5)
|
|
49
|
+
@data = data
|
|
50
|
+
@size = data.bytesize
|
|
51
|
+
@timeout = timeout
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def read(socket)
|
|
55
|
+
read_with_deadline(socket, @size, Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def write(socket)
|
|
59
|
+
write_with_deadline(socket, @data, Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class FramedIO
|
|
64
|
+
include Aikido::Zen::IPC::FramedIO
|
|
65
|
+
|
|
66
|
+
def initialize(data, timeout: 5)
|
|
67
|
+
@data = data
|
|
68
|
+
@timeout = timeout
|
|
69
|
+
@buffer = String.new(encoding: Encoding::BINARY)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def read(socket)
|
|
73
|
+
read_coalesced_frame_with_timeout(socket, @buffer, nil, @timeout)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def write(socket)
|
|
77
|
+
write_coalesced_frame_with_timeout(socket, @data, nil, @timeout)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def run(duration, io)
|
|
82
|
+
secret = SecureRandom.bytes(32)
|
|
83
|
+
|
|
84
|
+
server = Aikido::Zen::IPC::Server.start(secret) do |socket|
|
|
85
|
+
loop do
|
|
86
|
+
io.read(socket)
|
|
87
|
+
end
|
|
88
|
+
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
|
|
89
|
+
# disconnected
|
|
90
|
+
rescue IOError
|
|
91
|
+
# client stopped
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
client = Aikido::Zen::IPC::Client.new(secret, server.host, server.port)
|
|
95
|
+
|
|
96
|
+
sent = 0
|
|
97
|
+
|
|
98
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration
|
|
99
|
+
|
|
100
|
+
result = Benchmark.measure do
|
|
101
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
102
|
+
io.write(client.socket)
|
|
103
|
+
|
|
104
|
+
sent += 1
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
[sent, result.real]
|
|
109
|
+
ensure
|
|
110
|
+
client.close
|
|
111
|
+
server.stop
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def report(label, sent, real, data_size, baseline: nil)
|
|
115
|
+
rate = sent / real
|
|
116
|
+
mib_per_sec = rate * data_size / (1024 * 1024)
|
|
117
|
+
|
|
118
|
+
puts label
|
|
119
|
+
puts "#{rate.round} messages/sec, #{mib_per_sec.round(2)} MiB/sec"
|
|
120
|
+
|
|
121
|
+
if baseline
|
|
122
|
+
overhead = 100 - (100.0 * rate / baseline)
|
|
123
|
+
puts "#{overhead.round(1)}% fewer messages/sec than baseline"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if __FILE__ == $0
|
|
128
|
+
duration = (ENV["DURATION"] || 5).to_i
|
|
129
|
+
data_size = (ENV["DATA_SIZE"] || 1024).to_i
|
|
130
|
+
|
|
131
|
+
data = SecureRandom.bytes(data_size)
|
|
132
|
+
|
|
133
|
+
basic_io = BasicIO.new(data)
|
|
134
|
+
basic_io_sent, basic_io_real = run(duration, basic_io)
|
|
135
|
+
report("baseline", basic_io_sent, basic_io_real, data_size)
|
|
136
|
+
basic_io_rate = basic_io_sent / basic_io_real
|
|
137
|
+
|
|
138
|
+
puts
|
|
139
|
+
|
|
140
|
+
timed_io = TimedIO.new(data)
|
|
141
|
+
timed_io_sent, timed_io_real = run(duration, timed_io)
|
|
142
|
+
report("TimedIO", timed_io_sent, timed_io_real, data_size, baseline: basic_io_rate)
|
|
143
|
+
|
|
144
|
+
puts
|
|
145
|
+
|
|
146
|
+
framed_io = FramedIO.new(data)
|
|
147
|
+
framed_io_sent, framed_io_real = run(duration, framed_io)
|
|
148
|
+
report("FramedIO (coalesced)", framed_io_sent, framed_io_real, data_size, baseline: basic_io_rate)
|
|
149
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "benchmark"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "logger"
|
|
6
|
+
require "aikido-zen"
|
|
7
|
+
|
|
8
|
+
def run(duration, concurrency)
|
|
9
|
+
secret = SecureRandom.bytes(32)
|
|
10
|
+
logger = Logger.new(File::NULL)
|
|
11
|
+
|
|
12
|
+
server = Aikido::Zen::RPC::Server.start(secret, logger: logger) do |server|
|
|
13
|
+
server.handle("echo") { |respond, value| respond.call(value) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
client = Aikido::Zen::RPC::Client.start(secret, server.host, server.port, logger: logger)
|
|
17
|
+
|
|
18
|
+
calls = Concurrent::AtomicFixnum.new(0)
|
|
19
|
+
|
|
20
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration
|
|
21
|
+
|
|
22
|
+
result = Benchmark.measure do
|
|
23
|
+
threads = concurrency.times.map do
|
|
24
|
+
Thread.new do
|
|
25
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
26
|
+
client.invoke("echo", 5.0, "hello")
|
|
27
|
+
|
|
28
|
+
calls.increment
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
threads.each(&:join)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
[calls.value, result.real]
|
|
37
|
+
ensure
|
|
38
|
+
client.stop
|
|
39
|
+
server.stop
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def report(label, total_calls, real)
|
|
43
|
+
rate = total_calls / real
|
|
44
|
+
latency_ms = real / total_calls * 1000
|
|
45
|
+
|
|
46
|
+
puts label
|
|
47
|
+
puts "#{rate.round} calls/sec, #{latency_ms.round(3)} ms/call"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if __FILE__ == $0
|
|
51
|
+
duration = (ENV["DURATION"] || 5).to_i
|
|
52
|
+
concurrency = (ENV["CONCURRENCY"] || 1).to_i
|
|
53
|
+
|
|
54
|
+
calls, real = run(duration, concurrency)
|
|
55
|
+
|
|
56
|
+
report("RPC echo (concurrency: #{concurrency})", calls, real)
|
|
57
|
+
end
|
data/docs/rails.md
CHANGED
|
@@ -27,6 +27,28 @@ Bundler.require(*Rails.groups)
|
|
|
27
27
|
That's it! Zen will start to run inside your app when it starts getting
|
|
28
28
|
requests.
|
|
29
29
|
|
|
30
|
+
## Running with multiple worker processes (Puma)
|
|
31
|
+
|
|
32
|
+
Zen can share rate limiting state, runtime settings, and runtime stats
|
|
33
|
+
across all of an app's worker processes. This only works if Rails is booted
|
|
34
|
+
once and then forked into workers, which on Puma requires
|
|
35
|
+
[`preload_app!`][puma-preload]:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# config/puma.rb
|
|
39
|
+
preload_app!
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Without `preload_app!`, each worker boots Rails independently, so Zen
|
|
43
|
+
tracks rate limits, settings, and stats separately per worker instead
|
|
44
|
+
of sharing them.
|
|
45
|
+
|
|
46
|
+
> [!NOTE]
|
|
47
|
+
> This only matters for multi-process setups; it doesn't apply to
|
|
48
|
+
> single-process or threaded-only deployments.
|
|
49
|
+
|
|
50
|
+
[puma-preload]: https://github.com/puma/puma#cluster-mode
|
|
51
|
+
|
|
30
52
|
## Rate limiting and user blocking
|
|
31
53
|
|
|
32
54
|
If you want to add the rate limiting feature to your app, modify your code like this:
|
data/lib/aikido/zen/actor.rb
CHANGED
|
@@ -40,11 +40,11 @@ module Aikido::Zen
|
|
|
40
40
|
class Actor
|
|
41
41
|
def self.from_json(data)
|
|
42
42
|
new(
|
|
43
|
-
id: data[
|
|
44
|
-
name: data[
|
|
45
|
-
ip: data[
|
|
46
|
-
first_seen_at: Time.at(data[
|
|
47
|
-
last_seen_at: Time.at(data[
|
|
43
|
+
id: data["id"],
|
|
44
|
+
name: data["name"],
|
|
45
|
+
ip: data["lastIpAddress"],
|
|
46
|
+
first_seen_at: Time.at(data["firstSeenAt"] / 1000),
|
|
47
|
+
last_seen_at: Time.at(data["lastSeenAt"] / 1000)
|
|
48
48
|
)
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -135,11 +135,11 @@ module Aikido::Zen
|
|
|
135
135
|
|
|
136
136
|
def as_json
|
|
137
137
|
{
|
|
138
|
-
id
|
|
139
|
-
name
|
|
140
|
-
lastIpAddress
|
|
141
|
-
firstSeenAt
|
|
142
|
-
lastSeenAt
|
|
138
|
+
"id" => id,
|
|
139
|
+
"name" => name,
|
|
140
|
+
"lastIpAddress" => ip,
|
|
141
|
+
"firstSeenAt" => first_seen_at.to_i * 1000,
|
|
142
|
+
"lastSeenAt" => last_seen_at.to_i * 1000
|
|
143
143
|
}.compact
|
|
144
144
|
end
|
|
145
145
|
end
|
data/lib/aikido/zen/agent.rb
CHANGED
|
@@ -19,14 +19,12 @@ module Aikido::Zen
|
|
|
19
19
|
def initialize(
|
|
20
20
|
config: Aikido::Zen.config,
|
|
21
21
|
collector: Aikido::Zen.collector,
|
|
22
|
-
detached_agent: Aikido::Zen.detached_agent,
|
|
23
22
|
worker: Aikido::Zen::Worker.new(config: config),
|
|
24
23
|
api_client: Aikido::Zen::APIClient.new(config: config),
|
|
25
24
|
api_stream: Aikido::Zen::APIStream.new(config: config)
|
|
26
25
|
)
|
|
27
26
|
@config = config
|
|
28
27
|
@collector = collector
|
|
29
|
-
@detached_agent = detached_agent
|
|
30
28
|
@worker = worker
|
|
31
29
|
@api_client = api_client
|
|
32
30
|
@api_stream = api_stream
|
|
@@ -61,35 +59,37 @@ module Aikido::Zen
|
|
|
61
59
|
return
|
|
62
60
|
end
|
|
63
61
|
|
|
62
|
+
# Code coverage is disabled here because the block is only called when
|
|
63
|
+
# the process exits.
|
|
64
|
+
# :nocov:
|
|
64
65
|
at_exit { stop! if started? }
|
|
66
|
+
# :nocov:
|
|
65
67
|
|
|
66
68
|
report(Events::Started.new(time: @started_at)) do |response|
|
|
67
|
-
if update_settings_from_runtime_config!(response)
|
|
69
|
+
if update_settings_from_runtime_config!(response, reason: "after start")
|
|
68
70
|
updated_settings!
|
|
69
|
-
|
|
71
|
+
# :nocov:
|
|
72
|
+
else
|
|
73
|
+
# empty
|
|
70
74
|
end
|
|
75
|
+
# :nocov:
|
|
71
76
|
rescue => err
|
|
72
77
|
@config.logger.error(err.message)
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
begin
|
|
76
|
-
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
77
|
-
@config.logger.info("Updated runtime firewall list")
|
|
81
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after start")
|
|
78
82
|
rescue => err
|
|
79
83
|
@config.logger.error(err.message)
|
|
80
84
|
end
|
|
81
85
|
|
|
82
86
|
if @config.realtime_settings_updates_enabled?
|
|
83
|
-
|
|
84
|
-
@
|
|
85
|
-
|
|
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.")
|
|
87
|
+
@api_stream.handle("config-updated") do |event|
|
|
88
|
+
@config.logger.debug("Received server-sent event: config-updated")
|
|
89
|
+
settings_updated(event)
|
|
92
90
|
end
|
|
91
|
+
|
|
92
|
+
@api_stream.start!
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
poll_for_setting_updates
|
|
@@ -177,13 +177,15 @@ module Aikido::Zen
|
|
|
177
177
|
|
|
178
178
|
heartbeat = @collector.flush
|
|
179
179
|
report(heartbeat) do |response|
|
|
180
|
-
if update_settings_from_runtime_config!(response)
|
|
180
|
+
if update_settings_from_runtime_config!(response, reason: "after heartbeat")
|
|
181
181
|
updated_settings!
|
|
182
|
-
@config.logger.info("Updated runtime settings after heartbeat")
|
|
183
182
|
|
|
184
|
-
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
185
|
-
|
|
183
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after heartbeat")
|
|
184
|
+
# :nocov:
|
|
185
|
+
else
|
|
186
|
+
# empty
|
|
186
187
|
end
|
|
188
|
+
# :nocov:
|
|
187
189
|
end
|
|
188
190
|
end
|
|
189
191
|
|
|
@@ -197,13 +199,15 @@ module Aikido::Zen
|
|
|
197
199
|
def poll_for_setting_updates
|
|
198
200
|
@worker.every(@config.polling_interval) do
|
|
199
201
|
if @api_client.should_fetch_settings?
|
|
200
|
-
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config)
|
|
202
|
+
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config, reason: "after polling")
|
|
201
203
|
updated_settings!
|
|
202
|
-
|
|
204
|
+
# :nocov:
|
|
205
|
+
else
|
|
206
|
+
# empty
|
|
203
207
|
end
|
|
208
|
+
# :nocov:
|
|
204
209
|
|
|
205
|
-
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
206
|
-
@config.logger.info("Updated runtime firewall list after polling")
|
|
210
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after polling")
|
|
207
211
|
end
|
|
208
212
|
end
|
|
209
213
|
end
|
|
@@ -214,13 +218,15 @@ module Aikido::Zen
|
|
|
214
218
|
updated_at = Time.at(event[:data]["configUpdatedAt"].to_i)
|
|
215
219
|
|
|
216
220
|
if should_fetch_settings?(updated_at)
|
|
217
|
-
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config)
|
|
221
|
+
if update_settings_from_runtime_config!(@api_client.fetch_runtime_config, reason: "after server-sent event")
|
|
218
222
|
updated_settings!
|
|
219
|
-
@config.logger.info("Updated runtime settings after server-side event")
|
|
220
223
|
|
|
221
|
-
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists)
|
|
222
|
-
|
|
224
|
+
update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after server-sent event")
|
|
225
|
+
# :nocov:
|
|
226
|
+
else
|
|
227
|
+
# empty
|
|
223
228
|
end
|
|
229
|
+
# :nocov:
|
|
224
230
|
end
|
|
225
231
|
end
|
|
226
232
|
|
|
@@ -239,22 +245,44 @@ module Aikido::Zen
|
|
|
239
245
|
end
|
|
240
246
|
|
|
241
247
|
# @param data [Hash]
|
|
242
|
-
# @
|
|
243
|
-
|
|
248
|
+
# @param reason [String] context appended to the log message when logged
|
|
249
|
+
# @return [Boolean] whether the settings were updated; or nil if another
|
|
250
|
+
# thread is already applying an update.
|
|
251
|
+
def update_settings_from_runtime_config!(data, reason:)
|
|
252
|
+
# :nocov:
|
|
244
253
|
return unless @runtime_config_update_mutex.try_lock
|
|
254
|
+
# :nocov:
|
|
245
255
|
begin
|
|
246
|
-
Aikido::Zen.
|
|
256
|
+
return false unless Aikido::Zen.api_cache.update_runtime_config(data)
|
|
257
|
+
|
|
258
|
+
if Aikido::Zen.runtime_settings.update_from_runtime_config_json(data)
|
|
259
|
+
@config.logger.info("Updated runtime settings #{reason}")
|
|
260
|
+
true
|
|
261
|
+
else
|
|
262
|
+
false
|
|
263
|
+
end
|
|
247
264
|
ensure
|
|
248
265
|
@runtime_config_update_mutex.unlock
|
|
249
266
|
end
|
|
250
267
|
end
|
|
251
268
|
|
|
252
269
|
# @param data [Hash]
|
|
253
|
-
# @
|
|
254
|
-
|
|
270
|
+
# @param reason [String] context appended to the log message when logged
|
|
271
|
+
# @return [Boolean] whether the settings were updated; or nil if another
|
|
272
|
+
# thread is already applying an update.
|
|
273
|
+
def update_settings_from_runtime_firewall_lists!(data, reason:)
|
|
274
|
+
# :nocov:
|
|
255
275
|
return unless @runtime_firewall_lists_update_mutex.try_lock
|
|
276
|
+
# :nocov:
|
|
256
277
|
begin
|
|
257
|
-
Aikido::Zen.
|
|
278
|
+
return false unless Aikido::Zen.api_cache.update_runtime_firewall_lists(data)
|
|
279
|
+
|
|
280
|
+
if Aikido::Zen.runtime_settings.update_from_runtime_firewall_lists_json(data)
|
|
281
|
+
@config.logger.info("Updated runtime firewall list #{reason}")
|
|
282
|
+
true
|
|
283
|
+
else
|
|
284
|
+
false
|
|
285
|
+
end
|
|
258
286
|
ensure
|
|
259
287
|
@runtime_firewall_lists_update_mutex.unlock
|
|
260
288
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen
|
|
4
|
+
class APICache
|
|
5
|
+
attr_reader :runtime_config
|
|
6
|
+
attr_reader :runtime_firewall_lists
|
|
7
|
+
attr_reader :runtime_config_generation
|
|
8
|
+
attr_reader :runtime_firewall_lists_generation
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@runtime_config_generation = 0
|
|
12
|
+
@runtime_firewall_lists_generation = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def runtime_config=(value)
|
|
16
|
+
update_runtime_config(value)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def runtime_firewall_lists=(value)
|
|
20
|
+
update_runtime_firewall_lists(value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param value [Hash, nil]
|
|
24
|
+
# @return [Boolean] whether the value actually changed
|
|
25
|
+
def update_runtime_config(value)
|
|
26
|
+
return false if value == @runtime_config
|
|
27
|
+
|
|
28
|
+
@runtime_config_generation += 1
|
|
29
|
+
@runtime_config = value
|
|
30
|
+
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param value [Hash, nil]
|
|
35
|
+
# @return [Boolean] whether the value actually changed
|
|
36
|
+
def update_runtime_firewall_lists(value)
|
|
37
|
+
return false if value == @runtime_firewall_lists
|
|
38
|
+
|
|
39
|
+
@runtime_firewall_lists_generation += 1
|
|
40
|
+
@runtime_firewall_lists = value
|
|
41
|
+
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param known_generation [Integer, nil]
|
|
46
|
+
# @return [Array(Object, Integer), nil] the current value and generation,
|
|
47
|
+
# or nil if the known generation is already current.
|
|
48
|
+
def config_if_changed(known_generation)
|
|
49
|
+
return nil if known_generation == runtime_config_generation
|
|
50
|
+
[runtime_config, runtime_config_generation]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param known_generation [Integer, nil]
|
|
54
|
+
# @return [Array(Object, Integer), nil] the current value and generation,
|
|
55
|
+
# or nil if the known generation is already current.
|
|
56
|
+
def firewall_lists_if_changed(known_generation)
|
|
57
|
+
return nil if known_generation == runtime_firewall_lists_generation
|
|
58
|
+
[runtime_firewall_lists, runtime_firewall_lists_generation]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -27,7 +27,7 @@ module Aikido::Zen
|
|
|
27
27
|
@thread = nil
|
|
28
28
|
@http = nil
|
|
29
29
|
|
|
30
|
-
endpoint = @config.
|
|
30
|
+
endpoint = @config.realtime_endpoint
|
|
31
31
|
|
|
32
32
|
@host = endpoint.host
|
|
33
33
|
@port = endpoint.port
|
|
@@ -37,31 +37,6 @@ module Aikido::Zen
|
|
|
37
37
|
@handlers = Concurrent::Array.new
|
|
38
38
|
end
|
|
39
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
40
|
def running?
|
|
66
41
|
@running.true?
|
|
67
42
|
end
|
|
@@ -141,12 +116,18 @@ module Aikido::Zen
|
|
|
141
116
|
http.open_timeout = @open_timeout
|
|
142
117
|
http.write_timeout = @write_timeout
|
|
143
118
|
http.read_timeout = @read_timeout
|
|
119
|
+
# Working around Net::HTTP cleverness; retries GET requests once by default.
|
|
144
120
|
http.max_retries = 0
|
|
145
121
|
|
|
146
122
|
request = Net::HTTP::Get.new("/api/runtime/stream")
|
|
147
123
|
request["Authorization"] = @token
|
|
148
124
|
request["Accept"] = "text/event-stream"
|
|
149
125
|
request["Cache-Control"] = "no-cache"
|
|
126
|
+
request["X-Agent-Platform"] = "ruby"
|
|
127
|
+
request["X-Agent-Version"] = Aikido::Zen::VERSION
|
|
128
|
+
# Working around Net::HTTP cleverness; auto-negotiates gzip and buffers
|
|
129
|
+
# decompressed output internally.
|
|
130
|
+
request["Accept-Encoding"] = "identity"
|
|
150
131
|
|
|
151
132
|
@config.logger.debug("API stream connecting")
|
|
152
133
|
http.start
|
data/lib/aikido/zen/attack.rb
CHANGED
|
@@ -44,11 +44,11 @@ module Aikido::Zen
|
|
|
44
44
|
|
|
45
45
|
def as_json
|
|
46
46
|
{
|
|
47
|
-
kind
|
|
48
|
-
blocked
|
|
49
|
-
metadata
|
|
50
|
-
operation
|
|
51
|
-
stack
|
|
47
|
+
"kind" => kind,
|
|
48
|
+
"blocked" => blocked?,
|
|
49
|
+
"metadata" => metadata,
|
|
50
|
+
"operation" => @operation,
|
|
51
|
+
"stack" => @stack
|
|
52
52
|
}.compact.merge(input.as_json)
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -70,7 +70,7 @@ module Aikido::Zen
|
|
|
70
70
|
|
|
71
71
|
def metadata
|
|
72
72
|
{
|
|
73
|
-
filename
|
|
73
|
+
"filename" => filepath
|
|
74
74
|
}
|
|
75
75
|
end
|
|
76
76
|
|
|
@@ -107,7 +107,7 @@ module Aikido::Zen
|
|
|
107
107
|
|
|
108
108
|
def metadata
|
|
109
109
|
{
|
|
110
|
-
command
|
|
110
|
+
"command" => @command
|
|
111
111
|
}
|
|
112
112
|
end
|
|
113
113
|
|
|
@@ -139,9 +139,9 @@ module Aikido::Zen
|
|
|
139
139
|
|
|
140
140
|
def metadata
|
|
141
141
|
{
|
|
142
|
-
sql
|
|
143
|
-
dialect
|
|
144
|
-
failedToTokenize
|
|
142
|
+
"sql" => @query,
|
|
143
|
+
"dialect" => @dialect.name,
|
|
144
|
+
"failedToTokenize" => @failed_to_tokenize || nil
|
|
145
145
|
}.compact
|
|
146
146
|
end
|
|
147
147
|
|
|
@@ -174,8 +174,8 @@ module Aikido::Zen
|
|
|
174
174
|
|
|
175
175
|
def metadata
|
|
176
176
|
{
|
|
177
|
-
hostname
|
|
178
|
-
port
|
|
177
|
+
"hostname" => @request.uri.hostname,
|
|
178
|
+
"port" => @request.uri.port.to_s
|
|
179
179
|
}
|
|
180
180
|
end
|
|
181
181
|
end
|
|
@@ -212,8 +212,8 @@ module Aikido::Zen
|
|
|
212
212
|
|
|
213
213
|
def metadata
|
|
214
214
|
{
|
|
215
|
-
hostname
|
|
216
|
-
privateIP
|
|
215
|
+
"hostname" => @hostname,
|
|
216
|
+
"privateIP" => @address
|
|
217
217
|
}
|
|
218
218
|
end
|
|
219
219
|
end
|
|
@@ -69,9 +69,9 @@ module Aikido::Zen
|
|
|
69
69
|
|
|
70
70
|
def as_json
|
|
71
71
|
{
|
|
72
|
-
ipAddress
|
|
73
|
-
userAgent
|
|
74
|
-
source
|
|
72
|
+
"ipAddress" => @ip_address,
|
|
73
|
+
"userAgent" => @user_agent,
|
|
74
|
+
"source" => @source
|
|
75
75
|
}.compact
|
|
76
76
|
end
|
|
77
77
|
|
|
@@ -101,10 +101,10 @@ module Aikido::Zen
|
|
|
101
101
|
|
|
102
102
|
def as_json
|
|
103
103
|
{
|
|
104
|
-
metadata
|
|
105
|
-
samples
|
|
104
|
+
"metadata" => {
|
|
105
|
+
"samples" => @samples.as_json.to_json # The API only accepts string values in metadata
|
|
106
106
|
},
|
|
107
|
-
user
|
|
107
|
+
"user" => @user.as_json
|
|
108
108
|
}.compact
|
|
109
109
|
end
|
|
110
110
|
|
|
@@ -130,8 +130,8 @@ module Aikido::Zen
|
|
|
130
130
|
|
|
131
131
|
def as_json
|
|
132
132
|
{
|
|
133
|
-
method
|
|
134
|
-
url
|
|
133
|
+
"method" => @verb.as_json,
|
|
134
|
+
"url" => @path.as_json
|
|
135
135
|
}.compact
|
|
136
136
|
end
|
|
137
137
|
|