aikido-zen 0.1.1-arm64-darwin → 1.0.0.pre.beta.1-arm64-darwin
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 +7 -0
- data/CHANGELOG.md +4 -0
- data/README.md +11 -2
- data/benchmarks/README.md +8 -12
- data/benchmarks/rails7.1_sql_injection.js +30 -34
- data/docs/banner.svg +128 -129
- data/docs/config.md +8 -6
- data/docs/rails.md +1 -1
- data/lib/aikido/zen/agent.rb +13 -9
- data/lib/aikido/zen/api_client.rb +17 -7
- data/lib/aikido/zen/attack.rb +105 -36
- data/lib/aikido/zen/background_worker.rb +52 -0
- data/lib/aikido/zen/collector/routes.rb +2 -0
- data/lib/aikido/zen/collector.rb +31 -4
- data/lib/aikido/zen/config.rb +55 -20
- data/lib/aikido/zen/detached_agent/agent.rb +78 -0
- data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
- data/lib/aikido/zen/detached_agent/server.rb +41 -0
- data/lib/aikido/zen/detached_agent.rb +2 -0
- data/lib/aikido/zen/errors.rb +18 -1
- data/lib/aikido/zen/event.rb +4 -2
- data/lib/aikido/zen/libzen-v0.1.37.aarch64.dylib +0 -0
- data/lib/aikido/zen/middleware/check_allowed_addresses.rb +2 -14
- data/lib/aikido/zen/middleware/middleware.rb +11 -0
- data/lib/aikido/zen/middleware/{throttler.rb → rack_throttler.rb} +11 -13
- data/lib/aikido/zen/middleware/request_tracker.rb +190 -0
- data/lib/aikido/zen/middleware/set_context.rb +1 -4
- data/lib/aikido/zen/outbound_connection_monitor.rb +4 -0
- data/lib/aikido/zen/payload.rb +2 -0
- data/lib/aikido/zen/rails_engine.rb +12 -0
- data/lib/aikido/zen/rate_limiter/breaker.rb +3 -3
- data/lib/aikido/zen/rate_limiter.rb +7 -12
- data/lib/aikido/zen/request/rails_router.rb +6 -18
- data/lib/aikido/zen/request/schema/auth_schemas.rb +14 -0
- data/lib/aikido/zen/request/schema/builder.rb +0 -2
- data/lib/aikido/zen/request/schema/definition.rb +0 -5
- data/lib/aikido/zen/request/schema.rb +18 -3
- data/lib/aikido/zen/runtime_settings.rb +2 -2
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
- data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +4 -6
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +33 -21
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +15 -7
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +6 -0
- data/lib/aikido/zen/scanners.rb +2 -0
- data/lib/aikido/zen/sink.rb +6 -1
- data/lib/aikido/zen/sinks/action_controller.rb +34 -15
- data/lib/aikido/zen/sinks/file.rb +120 -0
- data/lib/aikido/zen/sinks/kernel.rb +73 -0
- data/lib/aikido/zen/sinks/socket.rb +13 -0
- data/lib/aikido/zen/sinks.rb +8 -0
- data/lib/aikido/zen/system_info.rb +1 -1
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen/worker.rb +5 -0
- data/lib/aikido/zen.rb +54 -8
- data/tasklib/bench.rake +31 -7
- data/tasklib/wrk.rb +88 -0
- metadata +22 -8
- data/lib/aikido/zen/libzen-v0.1.31.aarch64.dylib +0 -0
@@ -3,26 +3,34 @@
|
|
3
3
|
module Aikido::Zen
|
4
4
|
module Sinks
|
5
5
|
module ActionController
|
6
|
-
# Implements the "middleware" for
|
7
|
-
# need to check at the end of the `before_action`
|
8
|
-
# an actual Rack middleware, to allow for calls to
|
9
|
-
# made from before_actions in the host app, thus allowing
|
10
|
-
# by user ID rather than solely by IP.
|
11
|
-
class
|
6
|
+
# Implements the "middleware" for blocking requests (i.e.: rate-limiting or blocking
|
7
|
+
# user/bots) in Rails apps, where we need to check at the end of the `before_action`
|
8
|
+
# chain, rather than in an actual Rack middleware, to allow for calls to
|
9
|
+
# `Zen.track_user` being made from before_actions in the host app, thus allowing
|
10
|
+
# block/rate-limit by user ID rather than solely by IP.
|
11
|
+
class BlockRequestChecker
|
12
12
|
def initialize(
|
13
13
|
config: Aikido::Zen.config,
|
14
14
|
settings: Aikido::Zen.runtime_settings,
|
15
|
-
|
15
|
+
detached_agent: Aikido::Zen.detached_agent
|
16
16
|
)
|
17
17
|
@config = config
|
18
18
|
@settings = settings
|
19
|
-
@
|
19
|
+
@detached_agent = detached_agent
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
22
|
+
def block?(controller)
|
23
23
|
context = controller.request.env[Aikido::Zen::ENV_KEY]
|
24
24
|
request = context.request
|
25
25
|
|
26
|
+
if should_block_user?(request)
|
27
|
+
status, headers, body = @config.blocked_responder.call(request, :user)
|
28
|
+
controller.headers.update(headers)
|
29
|
+
controller.render plain: Array(body).join, status: status
|
30
|
+
|
31
|
+
return true
|
32
|
+
end
|
33
|
+
|
26
34
|
if should_throttle?(request)
|
27
35
|
status, headers, body = @config.rate_limited_responder.call(request)
|
28
36
|
controller.headers.update(headers)
|
@@ -35,14 +43,26 @@ module Aikido::Zen
|
|
35
43
|
end
|
36
44
|
|
37
45
|
private def should_throttle?(request)
|
46
|
+
return false unless @settings.endpoints[request.route].rate_limiting.enabled?
|
38
47
|
return false if @settings.skip_protection_for_ips.include?(request.ip)
|
39
48
|
|
40
|
-
@
|
49
|
+
result = @detached_agent.calculate_rate_limits(request)
|
50
|
+
return false unless result
|
51
|
+
|
52
|
+
request.env["aikido.rate_limiting"] = result
|
53
|
+
request.env["aikido.rate_limiting"].throttled?
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param request [Aikido::Zen::Request]
|
57
|
+
private def should_block_user?(request)
|
58
|
+
return false if request.actor.nil?
|
59
|
+
|
60
|
+
Aikido::Zen.runtime_settings.blocked_user_ids&.include?(request.actor.id)
|
41
61
|
end
|
42
62
|
end
|
43
63
|
|
44
|
-
def self.
|
45
|
-
@
|
64
|
+
def self.block_request_checker
|
65
|
+
@block_request_checker ||= Aikido::Zen::Sinks::ActionController::BlockRequestChecker.new
|
46
66
|
end
|
47
67
|
|
48
68
|
module Extensions
|
@@ -50,10 +70,9 @@ module Aikido::Zen
|
|
50
70
|
return super unless kind == :process_action
|
51
71
|
|
52
72
|
super do
|
53
|
-
|
54
|
-
throttled = rate_limiter.throttle(self)
|
73
|
+
checker = Aikido::Zen::Sinks::ActionController.block_request_checker
|
55
74
|
|
56
|
-
yield if block_given? && !
|
75
|
+
yield if block_given? && !checker.block?(self)
|
57
76
|
end
|
58
77
|
end
|
59
78
|
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen
|
4
|
+
module Sinks
|
5
|
+
module File
|
6
|
+
SINK = Sinks.add("File", scanners: [
|
7
|
+
Aikido::Zen::Scanners::PathTraversalScanner
|
8
|
+
])
|
9
|
+
|
10
|
+
module Extensions
|
11
|
+
def self.scan_path(filepath, operation)
|
12
|
+
SINK.scan(
|
13
|
+
filepath: filepath,
|
14
|
+
operation: operation
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Module to extend only the initializer method of `File` (`File.new`)
|
19
|
+
module Initiliazer
|
20
|
+
def initialize(filename, *, **)
|
21
|
+
Extensions.scan_path(filename, "new")
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def open(filename, *, **)
|
27
|
+
Extensions.scan_path(filename, "open")
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
def read(filename, *)
|
32
|
+
Extensions.scan_path(filename, "read")
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def write(filename, *, **)
|
37
|
+
Extensions.scan_path(filename, "write")
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def join(*)
|
42
|
+
joined = super
|
43
|
+
Extensions.scan_path(joined, "join")
|
44
|
+
joined
|
45
|
+
end
|
46
|
+
|
47
|
+
def chmod(mode, *paths)
|
48
|
+
paths.each { |path| Extensions.scan_path(path, "chmod") }
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
def chown(user, group, *paths)
|
53
|
+
paths.each { |path| Extensions.scan_path(path, "chown") }
|
54
|
+
super
|
55
|
+
end
|
56
|
+
|
57
|
+
def rename(from, to)
|
58
|
+
Extensions.scan_path(from, "rename")
|
59
|
+
Extensions.scan_path(to, "rename")
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
def symlink(from, to)
|
64
|
+
Extensions.scan_path(from, "symlink")
|
65
|
+
Extensions.scan_path(to, "symlink")
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
def truncate(file_name, *)
|
70
|
+
Extensions.scan_path(file_name, "truncate")
|
71
|
+
super
|
72
|
+
end
|
73
|
+
|
74
|
+
def unlink(*args)
|
75
|
+
args.each do |arg|
|
76
|
+
Extensions.scan_path(arg, "unlink")
|
77
|
+
end
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
def delete(*args)
|
82
|
+
args.each do |arg|
|
83
|
+
Extensions.scan_path(arg, "delete")
|
84
|
+
end
|
85
|
+
super
|
86
|
+
end
|
87
|
+
|
88
|
+
def utime(atime, mtime, *args)
|
89
|
+
args.each do |arg|
|
90
|
+
Extensions.scan_path(arg, "utime")
|
91
|
+
end
|
92
|
+
super
|
93
|
+
end
|
94
|
+
|
95
|
+
def expand_path(filename, *)
|
96
|
+
Extensions.scan_path(filename, "expand_path")
|
97
|
+
super
|
98
|
+
end
|
99
|
+
|
100
|
+
def realpath(filename, *)
|
101
|
+
Extensions.scan_path(filename, "realpath")
|
102
|
+
super
|
103
|
+
end
|
104
|
+
|
105
|
+
def realdirpath(filename, *)
|
106
|
+
Extensions.scan_path(filename, "realdirpath")
|
107
|
+
super
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Internally, Path Traversal's scanner logic uses `expand_path`, in order to avoid recursion issues we keep
|
115
|
+
# a copy of the original method, only to be used internally.
|
116
|
+
# It's important to keep this line before prepend the Extensions module, otherwise the alias will call
|
117
|
+
# the extended method.
|
118
|
+
::File.singleton_class.alias_method :expand_path__internal_for_aikido_zen, :expand_path
|
119
|
+
::File.singleton_class.prepend(Aikido::Zen::Sinks::File::Extensions)
|
120
|
+
::File.prepend Aikido::Zen::Sinks::File::Extensions::Initiliazer
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen
|
4
|
+
module Sinks
|
5
|
+
module Kernel
|
6
|
+
SINK = Sinks.add("Kernel", scanners: [
|
7
|
+
Aikido::Zen::Scanners::ShellInjectionScanner
|
8
|
+
])
|
9
|
+
|
10
|
+
module Extensions
|
11
|
+
# Checks if the user introduced input is trying to execute other commands
|
12
|
+
# using Shell Injection kind of attacks.
|
13
|
+
#
|
14
|
+
# @param command [String] the _full command_ that will be executed.
|
15
|
+
# @param context [Aikido::Zen::Context]
|
16
|
+
# @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
|
17
|
+
# @param operation [Symbol, String] name of the method being scanned.
|
18
|
+
#
|
19
|
+
# @return [Aikido::Zen::Attacks::ShellInjectionAttack, nil] an Attack if any
|
20
|
+
# user input is detected as part of a Shell Injection Attack, or +nil+ if it's safe.
|
21
|
+
def self.scan_command(command, operation)
|
22
|
+
SINK.scan(
|
23
|
+
command: command,
|
24
|
+
operation: operation
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
# `system, spawn` functions can be invoked in several ways. For more details,
|
29
|
+
# see [the documentation](https://ruby-doc.org/3.4.1/Kernel.html#method-i-spawn)
|
30
|
+
#
|
31
|
+
# In our context, we care primarily about two common scenarios:
|
32
|
+
# - one argument (String)
|
33
|
+
# e.g.: system("ls"), system("echo something")
|
34
|
+
# - two arguments (Hash, String)
|
35
|
+
# e.g.: system({"foo" => "bar"}, "ls"), system({"foo" => "bar"}, "echo something")
|
36
|
+
#
|
37
|
+
# In all other cases, we do not protect against shell argument injections. Specifically:
|
38
|
+
#
|
39
|
+
# If a user input contains something like $(whoami) and is passed as part of the command
|
40
|
+
# arguments (e.g., user_input = "$(whoami)"):
|
41
|
+
#
|
42
|
+
# system("echo", user_input) This is safe because Ruby automatically escapes arguments
|
43
|
+
# passed to system/spawn in this form.
|
44
|
+
#
|
45
|
+
# system("echo #{user_input}") This is not safe because Ruby interpolates the user_input
|
46
|
+
# into the command string, resulting in a potentially harmful
|
47
|
+
# command like `echo $(whoami)`.
|
48
|
+
def send_arg_to_scan(args, operation)
|
49
|
+
if args.size == 1 && args[0].is_a?(String)
|
50
|
+
Extensions.scan_command(args[0], operation)
|
51
|
+
end
|
52
|
+
|
53
|
+
if args.size == 2 && args[0].is_a?(Hash)
|
54
|
+
Extensions.scan_command(args[1], operation)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def system(*args, **)
|
59
|
+
send_arg_to_scan(args, "system")
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
def spawn(*args, **)
|
64
|
+
send_arg_to_scan(args, "spawn")
|
65
|
+
super
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
::Kernel.singleton_class.prepend Aikido::Zen::Sinks::Kernel::Extensions
|
73
|
+
::Kernel.prepend Aikido::Zen::Sinks::Kernel::Extensions
|
@@ -15,6 +15,19 @@ module Aikido::Zen
|
|
15
15
|
|
16
16
|
module IPSocketExtensions
|
17
17
|
def self.scan_socket(hostname, socket)
|
18
|
+
# We're patching IPSocket.open(..) method.
|
19
|
+
# The IPSocket class hierarchy is:
|
20
|
+
# IPSocket
|
21
|
+
# / \
|
22
|
+
# TCPSocket UDPSocket
|
23
|
+
# / \
|
24
|
+
# TCPServer SOCKSSocket
|
25
|
+
#
|
26
|
+
# Because we want to scan only HTTP requests, we skip in case the
|
27
|
+
# socket is not *exactly* an instance of TCPSocket — it's any
|
28
|
+
# of it subclasses
|
29
|
+
return unless socket.instance_of?(TCPSocket)
|
30
|
+
|
18
31
|
# ["AF_INET", 80, "10.0.0.1", "10.0.0.1"]
|
19
32
|
addr_family, *, remote_address = socket.peeraddr
|
20
33
|
|
data/lib/aikido/zen/sinks.rb
CHANGED
@@ -5,6 +5,14 @@ require_relative "sink"
|
|
5
5
|
require_relative "sinks/socket"
|
6
6
|
|
7
7
|
require_relative "sinks/action_controller" if defined?(::ActionController)
|
8
|
+
require_relative "sinks/file" if defined?(::File)
|
9
|
+
|
10
|
+
# Sadly, in ruby versions lower than 3.0, it's not possible to patch the
|
11
|
+
# Kernel module because how the `prepend` method is applied
|
12
|
+
# (https://stackoverflow.com/questions/78110397/prepend-kernel-module-function-globally#comment137713906_78112924)
|
13
|
+
if RUBY_VERSION >= "3.0"
|
14
|
+
require_relative "sinks/kernel" if defined?(::Kernel)
|
15
|
+
end
|
8
16
|
require_relative "sinks/resolv" if defined?(::Resolv)
|
9
17
|
require_relative "sinks/net_http" if defined?(::Net::HTTP)
|
10
18
|
require_relative "sinks/http" if defined?(::HTTP)
|
data/lib/aikido/zen/version.rb
CHANGED
data/lib/aikido/zen/worker.rb
CHANGED
data/lib/aikido/zen.rb
CHANGED
@@ -10,13 +10,16 @@ require_relative "zen/worker"
|
|
10
10
|
require_relative "zen/agent"
|
11
11
|
require_relative "zen/api_client"
|
12
12
|
require_relative "zen/context"
|
13
|
+
require_relative "zen/detached_agent"
|
14
|
+
require_relative "zen/middleware/check_allowed_addresses"
|
15
|
+
require_relative "zen/middleware/middleware"
|
16
|
+
require_relative "zen/middleware/request_tracker"
|
13
17
|
require_relative "zen/middleware/set_context"
|
14
18
|
require_relative "zen/outbound_connection"
|
15
19
|
require_relative "zen/outbound_connection_monitor"
|
16
20
|
require_relative "zen/runtime_settings"
|
17
21
|
require_relative "zen/rate_limiter"
|
18
22
|
require_relative "zen/scanners"
|
19
|
-
require_relative "zen/middleware/check_allowed_addresses"
|
20
23
|
require_relative "zen/rails_engine" if defined?(::Rails)
|
21
24
|
|
22
25
|
module Aikido
|
@@ -32,6 +35,10 @@ module Aikido
|
|
32
35
|
@runtime_settings ||= RuntimeSettings.new
|
33
36
|
end
|
34
37
|
|
38
|
+
def self.runtime_settings=(settings)
|
39
|
+
@runtime_settings = settings
|
40
|
+
end
|
41
|
+
|
35
42
|
# Gets information about the current system configuration, which is sent to
|
36
43
|
# the server along with any events.
|
37
44
|
def self.system_info
|
@@ -41,9 +48,15 @@ module Aikido
|
|
41
48
|
# Manages runtime metrics extracted from your app, which are uploaded to the
|
42
49
|
# Aikido servers if configured to do so.
|
43
50
|
def self.collector
|
51
|
+
check_and_handle_fork
|
44
52
|
@collector ||= Collector.new
|
45
53
|
end
|
46
54
|
|
55
|
+
def self.detached_agent
|
56
|
+
check_and_handle_fork
|
57
|
+
@detached_agent ||= DetachedAgent::Agent.new
|
58
|
+
end
|
59
|
+
|
47
60
|
# Gets the current context object that holds all information about the
|
48
61
|
# current request.
|
49
62
|
#
|
@@ -66,8 +79,11 @@ module Aikido
|
|
66
79
|
# @param request [Aikido::Zen::Request]
|
67
80
|
# @return [void]
|
68
81
|
def self.track_request(request)
|
69
|
-
|
70
|
-
|
82
|
+
collector.track_request
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.track_discovered_route(request)
|
86
|
+
collector.track_route(request)
|
71
87
|
end
|
72
88
|
|
73
89
|
# Tracks a network connection made to an external service.
|
@@ -75,7 +91,6 @@ module Aikido
|
|
75
91
|
# @param connection [Aikido::Zen::OutboundConnection]
|
76
92
|
# @return [void]
|
77
93
|
def self.track_outbound(connection)
|
78
|
-
autostart
|
79
94
|
collector.track_outbound(connection)
|
80
95
|
end
|
81
96
|
|
@@ -87,7 +102,6 @@ module Aikido
|
|
87
102
|
# @raise [Aikido::Zen::UnderAttackError] if the scan detected an Attack
|
88
103
|
# and blocking_mode is enabled.
|
89
104
|
def self.track_scan(scan)
|
90
|
-
autostart
|
91
105
|
collector.track_scan(scan)
|
92
106
|
agent.handle_attack(scan.attack) if scan.attack?
|
93
107
|
end
|
@@ -100,7 +114,6 @@ module Aikido
|
|
100
114
|
return if config.disabled?
|
101
115
|
|
102
116
|
if (actor = Aikido::Zen::Actor(user))
|
103
|
-
autostart
|
104
117
|
collector.track_user(actor)
|
105
118
|
current_context.request.actor = actor if current_context
|
106
119
|
else
|
@@ -113,6 +126,12 @@ module Aikido
|
|
113
126
|
end
|
114
127
|
end
|
115
128
|
|
129
|
+
# Marks that the Zen middleware was installed properly
|
130
|
+
# @return void
|
131
|
+
def self.middleware_installed!
|
132
|
+
collector.middleware_installed!
|
133
|
+
end
|
134
|
+
|
116
135
|
# Load all sinks matching libraries loaded into memory. This method should
|
117
136
|
# be called after all other dependencies have been loaded into memory (i.e.
|
118
137
|
# at the end of the initialization process).
|
@@ -127,7 +146,8 @@ module Aikido
|
|
127
146
|
# @!visibility private
|
128
147
|
# Stop any background threads.
|
129
148
|
def self.stop!
|
130
|
-
agent&.stop!
|
149
|
+
@agent&.stop!
|
150
|
+
@detached_agent_server&.stop!
|
131
151
|
end
|
132
152
|
|
133
153
|
# @!visibility private
|
@@ -136,8 +156,34 @@ module Aikido
|
|
136
156
|
@agent ||= Agent.start
|
137
157
|
end
|
138
158
|
|
159
|
+
def self.detached_agent_server
|
160
|
+
@detached_agent_server ||= DetachedAgent::Server.start!
|
161
|
+
end
|
162
|
+
|
139
163
|
class << self
|
140
|
-
|
164
|
+
# `agent` and `detached_agent` are started on the first method call.
|
165
|
+
# A mutex controls thread execution to prevent multiple attempts.
|
166
|
+
LOCK = Mutex.new
|
167
|
+
|
168
|
+
def start!
|
169
|
+
@pid = Process.pid
|
170
|
+
LOCK.synchronize do
|
171
|
+
agent
|
172
|
+
detached_agent_server
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def check_and_handle_fork
|
177
|
+
if has_forked
|
178
|
+
@detached_agent&.handle_fork
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def has_forked
|
183
|
+
pid_changed = Process.pid != @pid
|
184
|
+
@pid = Process.pid
|
185
|
+
pid_changed
|
186
|
+
end
|
141
187
|
end
|
142
188
|
end
|
143
189
|
end
|
data/tasklib/bench.rake
CHANGED
@@ -2,8 +2,11 @@
|
|
2
2
|
|
3
3
|
require "socket"
|
4
4
|
require "timeout"
|
5
|
+
require_relative "wrk"
|
5
6
|
|
6
7
|
SERVER_PIDS = {}
|
8
|
+
PORT_PROTECTED = 3001
|
9
|
+
PORT_UNPROTECTED = 3002
|
7
10
|
|
8
11
|
def stop_servers
|
9
12
|
SERVER_PIDS.each { |_, pid| Process.kill("TERM", pid) }
|
@@ -11,12 +14,15 @@ def stop_servers
|
|
11
14
|
end
|
12
15
|
|
13
16
|
def boot_server(dir, port:, env: {})
|
17
|
+
env["RAILS_MIN_THREADS"] = NUMBER_OF_THREADS
|
18
|
+
env["RAILS_MAX_THREADS"] = NUMBER_OF_THREADS
|
14
19
|
env["PORT"] = port.to_s
|
20
|
+
env["SECRET_KEY_BASE"] = rand(36**64).to_s(36)
|
15
21
|
|
16
22
|
Dir.chdir(dir) do
|
17
23
|
SERVER_PIDS[port] = Process.spawn(
|
18
24
|
env,
|
19
|
-
"rails", "server", "--pid", "#{Dir.pwd}/tmp/pids/server.#{port}.pid",
|
25
|
+
"rails", "server", "--pid", "#{Dir.pwd}/tmp/pids/server.#{port}.pid", "-e", "production",
|
20
26
|
out: "/dev/null"
|
21
27
|
)
|
22
28
|
rescue
|
@@ -48,8 +54,28 @@ end
|
|
48
54
|
Pathname.glob("sample_apps/*").select(&:directory?).each do |dir|
|
49
55
|
namespace :bench do
|
50
56
|
namespace dir.basename.to_s do
|
51
|
-
desc "Run benchmarks for the #{dir.basename} sample app"
|
52
|
-
task
|
57
|
+
desc "Run WRK benchmarks for the #{dir.basename} sample app"
|
58
|
+
task wrk_run: [:boot_protected_app, :boot_unprotected_app] do
|
59
|
+
throughput_decrease_limit_perc = 25
|
60
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.0.0") && Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.1.0")
|
61
|
+
# add higher limit for ruby 3.0
|
62
|
+
throughput_decrease_limit_perc = 35
|
63
|
+
end
|
64
|
+
|
65
|
+
wait_for_servers
|
66
|
+
run_benchmark(
|
67
|
+
route_zen: "http://localhost:#{PORT_PROTECTED}/benchmark", # Application with Zen
|
68
|
+
route_no_zen: "http://localhost:#{PORT_UNPROTECTED}/benchmark", # Application without Zen
|
69
|
+
description: "An empty route (1ms simulated delay)",
|
70
|
+
throughput_decrease_limit_perc: throughput_decrease_limit_perc,
|
71
|
+
latency_increase_limit_ms: 200
|
72
|
+
)
|
73
|
+
ensure
|
74
|
+
stop_servers
|
75
|
+
end
|
76
|
+
|
77
|
+
desc "Run K6 benchmarks for the #{dir.basename} sample app"
|
78
|
+
task k6_run: [:boot_protected_app, :boot_unprotected_app] do
|
53
79
|
wait_for_servers
|
54
80
|
Dir.chdir("benchmarks") { sh "k6 run #{dir.basename}.js" }
|
55
81
|
ensure
|
@@ -57,14 +83,12 @@ Pathname.glob("sample_apps/*").select(&:directory?).each do |dir|
|
|
57
83
|
end
|
58
84
|
|
59
85
|
task :boot_protected_app do
|
60
|
-
boot_server(dir, port:
|
86
|
+
boot_server(dir, port: PORT_PROTECTED)
|
61
87
|
end
|
62
88
|
|
63
89
|
task :boot_unprotected_app do
|
64
|
-
boot_server(dir, port:
|
90
|
+
boot_server(dir, port: PORT_UNPROTECTED, env: {"AIKIDO_DISABLED" => "true"})
|
65
91
|
end
|
66
92
|
end
|
67
|
-
|
68
|
-
task default: "#{dir.basename}:run"
|
69
93
|
end
|
70
94
|
end
|
data/tasklib/wrk.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require "open3"
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
NUMBER_OF_THREADS = ENV.fetch("BENCHMARK_NUMBER_OF_THREADS") { 12 }.to_s
|
5
|
+
CONNECTIONS = ENV.fetch("BENCHMARK_WRK_CONNECTIONS") { 400 }
|
6
|
+
|
7
|
+
def generate_wrk_command_for_url(url)
|
8
|
+
# Define the command with wrk included
|
9
|
+
"wrk --threads #{NUMBER_OF_THREADS} --connections #{CONNECTIONS} --duration 15s --timeout 5s --latency #{url}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def cold_start(url)
|
13
|
+
10.times do
|
14
|
+
_, err, status = Open3.capture3("curl #{url}")
|
15
|
+
|
16
|
+
if status != 0
|
17
|
+
puts err
|
18
|
+
exit(-1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def extract_requests_and_latency_tuple(out, err, status)
|
24
|
+
if status == 0
|
25
|
+
# Extracting requests/sec
|
26
|
+
requests_sec_match = out.match(/Requests\/sec:\s+([\d.]+)/)
|
27
|
+
requests_sec = requests_sec_match[1].to_f if requests_sec_match
|
28
|
+
|
29
|
+
# Extracting latency
|
30
|
+
latency_match = out.match(/Latency\s+([\d.]+)(ms|s)/)
|
31
|
+
latency = latency_match[1].to_f if latency_match
|
32
|
+
latency_unit = latency_match[2] if latency_match
|
33
|
+
|
34
|
+
if latency_unit == "s"
|
35
|
+
latency *= 1000
|
36
|
+
end
|
37
|
+
|
38
|
+
{requests_sec: requests_sec, latency: latency}
|
39
|
+
else
|
40
|
+
puts "Error occurred running benchmark command:"
|
41
|
+
puts err.strip
|
42
|
+
exit(1)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def run_benchmark(route_no_zen:, route_zen:, description:, throughput_decrease_limit_perc:, latency_increase_limit_ms:)
|
47
|
+
# Cold start
|
48
|
+
cold_start(route_no_zen)
|
49
|
+
cold_start(route_zen)
|
50
|
+
|
51
|
+
out, err, status = Open3.capture3(generate_wrk_command_for_url(route_zen))
|
52
|
+
puts <<~MSG
|
53
|
+
WRK OUTPUT
|
54
|
+
================
|
55
|
+
FIREWALL ENABLED:
|
56
|
+
#{out}
|
57
|
+
----------------
|
58
|
+
MSG
|
59
|
+
result_zen_enabled = extract_requests_and_latency_tuple(out, err, status)
|
60
|
+
|
61
|
+
out, err, status = Open3.capture3(generate_wrk_command_for_url(route_no_zen))
|
62
|
+
puts <<~MSG
|
63
|
+
FIREWALL DISABLED:
|
64
|
+
#{out}
|
65
|
+
================
|
66
|
+
MSG
|
67
|
+
result_zen_disabled = extract_requests_and_latency_tuple(out, err, status)
|
68
|
+
|
69
|
+
# Check if the command was successful
|
70
|
+
if result_zen_enabled && result_zen_disabled
|
71
|
+
# Print the output, which should be the Requests/sec value
|
72
|
+
puts "[ZEN ENABLED ] Requests/sec: #{result_zen_enabled[:requests_sec]} | Latency in ms: #{result_zen_enabled[:latency]}"
|
73
|
+
puts "[ZEN DISABLED] Requests/sec: #{result_zen_disabled[:requests_sec]} | Latency in ms: #{result_zen_disabled[:latency]}"
|
74
|
+
|
75
|
+
latency_increase_ms = (result_zen_enabled[:latency] - result_zen_disabled[:latency]).round(2)
|
76
|
+
puts "-> Delta in ms: #{latency_increase_ms}ms after running load test on #{description}"
|
77
|
+
|
78
|
+
throughput_decrease_perc = ((result_zen_disabled[:requests_sec] - result_zen_enabled[:requests_sec]) / result_zen_disabled[:requests_sec] * 100).round
|
79
|
+
puts "-> #{throughput_decrease_perc}% decrease in throughput after running load test on #{description}\n"
|
80
|
+
|
81
|
+
if latency_increase_ms >= latency_increase_limit_ms
|
82
|
+
exit(1)
|
83
|
+
end
|
84
|
+
if throughput_decrease_perc >= throughput_decrease_limit_perc
|
85
|
+
exit(1)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|