aikido-zen 1.0.2.beta.2-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 +7 -0
- data/.aikido +6 -0
- data/.ruby-version +1 -0
- data/.simplecov +26 -0
- data/.standard.yml +3 -0
- data/LICENSE +674 -0
- data/README.md +146 -0
- data/Rakefile +67 -0
- data/benchmarks/README.md +23 -0
- data/benchmarks/rails7.1_sql_injection.js +70 -0
- data/docs/banner.svg +202 -0
- data/docs/config.md +125 -0
- data/docs/proxy.md +10 -0
- data/docs/rails.md +114 -0
- data/lib/aikido/zen/actor.rb +116 -0
- data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
- data/lib/aikido/zen/agent.rb +179 -0
- data/lib/aikido/zen/api_client.rb +145 -0
- data/lib/aikido/zen/attack.rb +207 -0
- data/lib/aikido/zen/background_worker.rb +52 -0
- data/lib/aikido/zen/capped_collections.rb +68 -0
- data/lib/aikido/zen/collector/hosts.rb +15 -0
- data/lib/aikido/zen/collector/routes.rb +66 -0
- data/lib/aikido/zen/collector/sink_stats.rb +95 -0
- data/lib/aikido/zen/collector/stats.rb +111 -0
- data/lib/aikido/zen/collector/users.rb +30 -0
- data/lib/aikido/zen/collector.rb +144 -0
- data/lib/aikido/zen/config.rb +282 -0
- data/lib/aikido/zen/context/rack_request.rb +24 -0
- data/lib/aikido/zen/context/rails_request.rb +44 -0
- data/lib/aikido/zen/context.rb +112 -0
- 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 +78 -0
- data/lib/aikido/zen/detached_agent.rb +2 -0
- data/lib/aikido/zen/errors.rb +107 -0
- data/lib/aikido/zen/event.rb +71 -0
- data/lib/aikido/zen/internals.rb +103 -0
- data/lib/aikido/zen/libzen-v0.1.39-aarch64-linux.so +0 -0
- data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
- data/lib/aikido/zen/middleware/middleware.rb +11 -0
- data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
- data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
- data/lib/aikido/zen/middleware/set_context.rb +26 -0
- data/lib/aikido/zen/outbound_connection.rb +45 -0
- data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
- data/lib/aikido/zen/package.rb +22 -0
- data/lib/aikido/zen/payload.rb +50 -0
- data/lib/aikido/zen/rails_engine.rb +56 -0
- data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
- data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
- data/lib/aikido/zen/rate_limiter/result.rb +31 -0
- data/lib/aikido/zen/rate_limiter.rb +50 -0
- data/lib/aikido/zen/request/heuristic_router.rb +115 -0
- data/lib/aikido/zen/request/rails_router.rb +77 -0
- data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
- data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
- data/lib/aikido/zen/request/schema/builder.rb +121 -0
- data/lib/aikido/zen/request/schema/definition.rb +107 -0
- data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
- data/lib/aikido/zen/request/schema.rb +87 -0
- data/lib/aikido/zen/request.rb +122 -0
- data/lib/aikido/zen/route.rb +39 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
- data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
- data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
- data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
- data/lib/aikido/zen/runtime_settings.rb +65 -0
- data/lib/aikido/zen/scan.rb +75 -0
- 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 +93 -0
- data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
- data/lib/aikido/zen/scanners.rb +7 -0
- data/lib/aikido/zen/sink.rb +118 -0
- data/lib/aikido/zen/sinks/action_controller.rb +83 -0
- data/lib/aikido/zen/sinks/async_http.rb +80 -0
- data/lib/aikido/zen/sinks/curb.rb +113 -0
- data/lib/aikido/zen/sinks/em_http.rb +83 -0
- data/lib/aikido/zen/sinks/excon.rb +118 -0
- data/lib/aikido/zen/sinks/file.rb +112 -0
- data/lib/aikido/zen/sinks/http.rb +93 -0
- data/lib/aikido/zen/sinks/httpclient.rb +95 -0
- data/lib/aikido/zen/sinks/httpx.rb +78 -0
- data/lib/aikido/zen/sinks/kernel.rb +33 -0
- data/lib/aikido/zen/sinks/mysql2.rb +31 -0
- data/lib/aikido/zen/sinks/net_http.rb +101 -0
- data/lib/aikido/zen/sinks/patron.rb +103 -0
- data/lib/aikido/zen/sinks/pg.rb +72 -0
- data/lib/aikido/zen/sinks/resolv.rb +62 -0
- data/lib/aikido/zen/sinks/socket.rb +78 -0
- data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
- data/lib/aikido/zen/sinks/trilogy.rb +31 -0
- data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
- data/lib/aikido/zen/sinks.rb +36 -0
- data/lib/aikido/zen/sinks_dsl.rb +250 -0
- data/lib/aikido/zen/synchronizable.rb +24 -0
- data/lib/aikido/zen/system_info.rb +84 -0
- data/lib/aikido/zen/version.rb +10 -0
- data/lib/aikido/zen/worker.rb +87 -0
- data/lib/aikido/zen.rb +246 -0
- data/lib/aikido-zen.rb +3 -0
- data/placeholder/.gitignore +4 -0
- data/placeholder/README.md +11 -0
- data/placeholder/Rakefile +75 -0
- data/placeholder/lib/placeholder.rb.template +3 -0
- data/placeholder/placeholder.gemspec.template +20 -0
- data/tasklib/bench.rake +94 -0
- data/tasklib/libzen.rake +133 -0
- data/tasklib/wrk.rb +88 -0
- metadata +205 -0
data/lib/aikido/zen.rb
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "zen/version"
|
4
|
+
require_relative "zen/errors"
|
5
|
+
require_relative "zen/actor"
|
6
|
+
require_relative "zen/config"
|
7
|
+
require_relative "zen/collector"
|
8
|
+
require_relative "zen/system_info"
|
9
|
+
require_relative "zen/worker"
|
10
|
+
require_relative "zen/agent"
|
11
|
+
require_relative "zen/api_client"
|
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"
|
17
|
+
require_relative "zen/middleware/set_context"
|
18
|
+
require_relative "zen/outbound_connection"
|
19
|
+
require_relative "zen/outbound_connection_monitor"
|
20
|
+
require_relative "zen/runtime_settings"
|
21
|
+
require_relative "zen/rate_limiter"
|
22
|
+
require_relative "zen/scanners"
|
23
|
+
|
24
|
+
module Aikido
|
25
|
+
module Zen
|
26
|
+
# Enable protection. Until this method is called no sinks are loaded
|
27
|
+
# and the Aikido Agent does not start.
|
28
|
+
#
|
29
|
+
# This method should be called only once, in the application after the
|
30
|
+
# initialization process is complete.
|
31
|
+
#
|
32
|
+
# @return [void]
|
33
|
+
def self.protect!
|
34
|
+
if config.disabled?
|
35
|
+
config.logger.warn("Zen has been disabled and will not run.")
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
return unless config.protect?
|
40
|
+
|
41
|
+
unless load_sources! && load_sinks!
|
42
|
+
config.logger.warn("Zen could not find any supported libraries or frameworks. Visit https://github.com/AikidoSec/firewall-ruby for more information.")
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
middleware_installed!
|
47
|
+
end
|
48
|
+
|
49
|
+
# @!visibility private
|
50
|
+
# Returns whether the loaded gem specification satisfies the listed requirements.
|
51
|
+
#
|
52
|
+
# Returns false if the gem specification is not loaded.
|
53
|
+
#
|
54
|
+
# @param name [String] the gem name
|
55
|
+
# @param requirements [Array<String>] a variable number of gem requirement strings
|
56
|
+
#
|
57
|
+
# @return [Boolean] true if the gem specification is loaded and all gem requirements are satisfied
|
58
|
+
def self.satisfy(name, *requirements)
|
59
|
+
spec = Gem.loaded_specs[name]
|
60
|
+
|
61
|
+
return false if spec.nil?
|
62
|
+
|
63
|
+
Gem::Requirement.new(*requirements).satisfied_by?(spec.version)
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Aikido::Zen::Config] the agent configuration.
|
67
|
+
def self.config
|
68
|
+
@config ||= Config.new
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Aikido::Zen::RuntimeSettings] the firewall configuration sourced
|
72
|
+
# from your Aikido dashboard. This is periodically polled for updates.
|
73
|
+
def self.runtime_settings
|
74
|
+
@runtime_settings ||= RuntimeSettings.new
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.runtime_settings=(settings)
|
78
|
+
@runtime_settings = settings
|
79
|
+
end
|
80
|
+
|
81
|
+
# Gets information about the current system configuration, which is sent to
|
82
|
+
# the server along with any events.
|
83
|
+
def self.system_info
|
84
|
+
@system_info ||= SystemInfo.new
|
85
|
+
end
|
86
|
+
|
87
|
+
# Manages runtime metrics extracted from your app, which are uploaded to the
|
88
|
+
# Aikido servers if configured to do so.
|
89
|
+
def self.collector
|
90
|
+
check_and_handle_fork
|
91
|
+
@collector ||= Collector.new
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.detached_agent
|
95
|
+
check_and_handle_fork
|
96
|
+
@detached_agent ||= DetachedAgent::Agent.new
|
97
|
+
end
|
98
|
+
|
99
|
+
# Gets the current context object that holds all information about the
|
100
|
+
# current request.
|
101
|
+
#
|
102
|
+
# @return [Aikido::Zen::Context, nil]
|
103
|
+
def self.current_context
|
104
|
+
Thread.current[:_aikido_current_context_]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Sets the current context object that holds all information about the
|
108
|
+
# current request, or +nil+ to clear the current context.
|
109
|
+
#
|
110
|
+
# @param context [Aikido::Zen::Context, nil]
|
111
|
+
# @return [Aikido::Zen::Context, nil]
|
112
|
+
def self.current_context=(context)
|
113
|
+
Thread.current[:_aikido_current_context_] = context
|
114
|
+
end
|
115
|
+
|
116
|
+
# Track statistics about an HTTP request the app is handling.
|
117
|
+
#
|
118
|
+
# @param request [Aikido::Zen::Request]
|
119
|
+
# @return [void]
|
120
|
+
def self.track_request(request)
|
121
|
+
collector.track_request
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.track_discovered_route(request)
|
125
|
+
collector.track_route(request)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Tracks a network connection made to an external service.
|
129
|
+
#
|
130
|
+
# @param connection [Aikido::Zen::OutboundConnection]
|
131
|
+
# @return [void]
|
132
|
+
def self.track_outbound(connection)
|
133
|
+
collector.track_outbound(connection)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Track statistics about the result of a Sink's scan, and report it as
|
137
|
+
# an Attack if one is detected.
|
138
|
+
#
|
139
|
+
# @param scan [Aikido::Zen::Scan]
|
140
|
+
# @return [void]
|
141
|
+
# @raise [Aikido::Zen::UnderAttackError] if the scan detected an Attack
|
142
|
+
# and blocking_mode is enabled.
|
143
|
+
def self.track_scan(scan)
|
144
|
+
collector.track_scan(scan)
|
145
|
+
agent.handle_attack(scan.attack) if scan.attack?
|
146
|
+
end
|
147
|
+
|
148
|
+
# Track the user making the current request.
|
149
|
+
#
|
150
|
+
# @param (see Aikido::Zen.Actor)
|
151
|
+
# @return [void]
|
152
|
+
def self.track_user(user)
|
153
|
+
return if config.disabled?
|
154
|
+
|
155
|
+
if (actor = Aikido::Zen::Actor(user))
|
156
|
+
collector.track_user(actor)
|
157
|
+
current_context.request.actor = actor if current_context
|
158
|
+
else
|
159
|
+
config.logger.warn(format(<<~LOG, obj: user))
|
160
|
+
Incompatible object sent to track_user: %<obj>p
|
161
|
+
|
162
|
+
The object must either implement #to_aikido_actor, or be a Hash with
|
163
|
+
an :id (or "id") and, optionally, a :name (or "name") key.
|
164
|
+
LOG
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Align with other Zen implementations, while keeping internal consistency.
|
169
|
+
class << self
|
170
|
+
alias_method :set_user, :track_user
|
171
|
+
end
|
172
|
+
|
173
|
+
# Marks that the Zen middleware was installed properly
|
174
|
+
# @return void
|
175
|
+
def self.middleware_installed!
|
176
|
+
collector.middleware_installed!
|
177
|
+
end
|
178
|
+
|
179
|
+
# @!visibility private
|
180
|
+
# Load all sources.
|
181
|
+
#
|
182
|
+
# @return [Boolean] true if any sources were loaded
|
183
|
+
def self.load_sources!
|
184
|
+
if Aikido::Zen.satisfy("rails", ">= 7.0")
|
185
|
+
require_relative "zen/rails_engine"
|
186
|
+
|
187
|
+
return true
|
188
|
+
end
|
189
|
+
|
190
|
+
false
|
191
|
+
end
|
192
|
+
|
193
|
+
# @!visibility private
|
194
|
+
# Load all sinks.
|
195
|
+
#
|
196
|
+
# @return [Boolean] true if any sinks were loaded
|
197
|
+
def self.load_sinks!
|
198
|
+
require_relative "zen/sinks"
|
199
|
+
|
200
|
+
!Aikido::Zen::Sinks.registry.empty?
|
201
|
+
end
|
202
|
+
|
203
|
+
# @!visibility private
|
204
|
+
# Stop any background threads.
|
205
|
+
def self.stop!
|
206
|
+
@agent&.stop!
|
207
|
+
@detached_agent_server&.stop!
|
208
|
+
end
|
209
|
+
|
210
|
+
# @!visibility private
|
211
|
+
# Starts the background agent if it has not been started yet.
|
212
|
+
def self.agent
|
213
|
+
@agent ||= Agent.start
|
214
|
+
end
|
215
|
+
|
216
|
+
def self.detached_agent_server
|
217
|
+
@detached_agent_server ||= DetachedAgent::Server.start
|
218
|
+
end
|
219
|
+
|
220
|
+
class << self
|
221
|
+
# `agent` and `detached_agent` are started on the first method call.
|
222
|
+
# A mutex controls thread execution to prevent multiple attempts.
|
223
|
+
LOCK = Mutex.new
|
224
|
+
|
225
|
+
def start!
|
226
|
+
@pid = Process.pid
|
227
|
+
LOCK.synchronize do
|
228
|
+
agent
|
229
|
+
detached_agent_server
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def check_and_handle_fork
|
234
|
+
if has_forked
|
235
|
+
@detached_agent&.handle_fork
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def has_forked
|
240
|
+
pid_changed = Process.pid != @pid
|
241
|
+
@pid = Process.pid
|
242
|
+
pid_changed
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
data/lib/aikido-zen.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Security Placeholder
|
2
|
+
|
3
|
+
This gem has been published by [Aikido Security](https://aikido.dev) to help prevent supply chain attacks and protect the integrity of the Ruby ecosystem.
|
4
|
+
|
5
|
+
It is **not intended for direct use** and contains no functional code.
|
6
|
+
|
7
|
+
If you are looking for the actual library, please use [`aikido-zen`](https://rubygems.org/gems/aikido-zen).
|
8
|
+
|
9
|
+
---
|
10
|
+
|
11
|
+
This package exists solely for security purposes. For more information, visit [aikido.dev](https://aikido.dev).
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rake"
|
4
|
+
require "rake/clean"
|
5
|
+
require "rubygems/package"
|
6
|
+
require "fileutils"
|
7
|
+
|
8
|
+
GEM_NAMES = %w[aikido]
|
9
|
+
|
10
|
+
# Clean up created files
|
11
|
+
CLEAN.include("LICENSE")
|
12
|
+
CLEAN.include(*GEM_NAMES.map { |name| "#{name}.gemspec" })
|
13
|
+
CLEAN.include(*GEM_NAMES.map { |name| "lib/#{name}.rb" })
|
14
|
+
CLOBBER.include(*GEM_NAMES.map { |name| "#{name}-*.gem" })
|
15
|
+
|
16
|
+
namespace :build do
|
17
|
+
GEM_NAMES.each do |gem_name|
|
18
|
+
file "LICENSE" => ["../LICENSE"] do
|
19
|
+
FileUtils.cp("../LICENSE", "LICENSE")
|
20
|
+
puts "Copied LICENSE"
|
21
|
+
end
|
22
|
+
|
23
|
+
entry_point_path = "lib/#{gem_name}.rb"
|
24
|
+
|
25
|
+
# Generate the entry point file from template if needed
|
26
|
+
file entry_point_path => ["lib/placeholder.rb.template"] do
|
27
|
+
template = File.read("lib/placeholder.rb.template")
|
28
|
+
content = template.gsub("@GEM_NAME", gem_name)
|
29
|
+
File.write(entry_point_path, content)
|
30
|
+
puts "Generated #{entry_point_path}"
|
31
|
+
end
|
32
|
+
|
33
|
+
gemspec_path = "#{gem_name}.gemspec"
|
34
|
+
|
35
|
+
# Generate gemspec file from template if needed
|
36
|
+
file gemspec_path => ["placeholder.gemspec.template"] do
|
37
|
+
template = File.read("placeholder.gemspec.template")
|
38
|
+
content = template.gsub("@GEM_NAME", gem_name)
|
39
|
+
File.write(gemspec_path, content)
|
40
|
+
puts "Generated #{gemspec_path}"
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "Build the #{gem_name} gem"
|
44
|
+
task gem_name => [entry_point_path, gemspec_path, "LICENSE"] do
|
45
|
+
gemspec = Gem::Specification.load(gemspec_path)
|
46
|
+
raise "Failed to load gemspec: #{gemspec_path}" unless gemspec
|
47
|
+
|
48
|
+
gem_path = Gem::Package.build(gemspec)
|
49
|
+
puts "Built #{gem_path}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "Build all gems"
|
54
|
+
task all: GEM_NAMES.map { |gem_name| "build:#{gem_name}" }
|
55
|
+
end
|
56
|
+
|
57
|
+
namespace :release do
|
58
|
+
GEM_NAMES.each do |gem_name|
|
59
|
+
gemspec_path = "#{gem_name}.gemspec"
|
60
|
+
|
61
|
+
desc "Build and publish the #{gem_name} to RubyGems"
|
62
|
+
task gem_name => ["build:#{gem_name}"] do
|
63
|
+
gemspec = Gem::Specification.load(gemspec_path)
|
64
|
+
raise "Failed to load gemspec: #{gemspec_path}" unless gemspec
|
65
|
+
|
66
|
+
gem_path = "#{gemspec.name}-#{gemspec.version}.gem"
|
67
|
+
|
68
|
+
puts "Publishing #{gem_path} to RubyGem..."
|
69
|
+
sh "gem push #{gem_path}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
desc "Build and publish all gems to RubyGems"
|
74
|
+
task all: GEM_NAMES.map { |gem_name| "release:#{gem_name}" }
|
75
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "@GEM_NAME"
|
5
|
+
spec.version = "0.0.2"
|
6
|
+
spec.authors = ["Aikido Security"]
|
7
|
+
spec.email = ["dev-admin@aikido.dev"]
|
8
|
+
spec.summary = "Security placeholder for 'aikido-zen'."
|
9
|
+
spec.description = "This gem has been published by Aikido Security to help prevent supply chain attacks. It is not intended for direct use. Please use 'aikido-zen' instead."
|
10
|
+
spec.homepage = "https://aikido.dev/zen"
|
11
|
+
spec.license = "AGPL-3.0-or-later"
|
12
|
+
|
13
|
+
spec.required_ruby_version = ">= 2.3"
|
14
|
+
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/aikidosec/firewall-ruby"
|
17
|
+
|
18
|
+
spec.files = ["lib/@GEM_NAME.rb", "README.md", "LICENSE"]
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
end
|
data/tasklib/bench.rake
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
require "timeout"
|
5
|
+
require_relative "wrk"
|
6
|
+
|
7
|
+
SERVER_PIDS = {}
|
8
|
+
PORT_PROTECTED = 3001
|
9
|
+
PORT_UNPROTECTED = 3002
|
10
|
+
|
11
|
+
def stop_servers
|
12
|
+
SERVER_PIDS.each { |_, pid| Process.kill("TERM", pid) }
|
13
|
+
SERVER_PIDS.clear
|
14
|
+
end
|
15
|
+
|
16
|
+
def boot_server(dir, port:, env: {})
|
17
|
+
env["RAILS_MIN_THREADS"] = NUMBER_OF_THREADS
|
18
|
+
env["RAILS_MAX_THREADS"] = NUMBER_OF_THREADS
|
19
|
+
env["PORT"] = port.to_s
|
20
|
+
env["SECRET_KEY_BASE"] = rand(36**64).to_s(36)
|
21
|
+
|
22
|
+
Dir.chdir(dir) do
|
23
|
+
SERVER_PIDS[port] = Process.spawn(
|
24
|
+
env,
|
25
|
+
"rails", "server", "--pid", "#{Dir.pwd}/tmp/pids/server.#{port}.pid", "-e", "production",
|
26
|
+
out: "/dev/null"
|
27
|
+
)
|
28
|
+
rescue
|
29
|
+
SERVER_PIDS.delete(port)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def port_open?(port, timeout: 1)
|
34
|
+
Timeout.timeout(timeout) do
|
35
|
+
TCPSocket.new("127.0.0.1", port).close
|
36
|
+
true
|
37
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError
|
38
|
+
false
|
39
|
+
end
|
40
|
+
rescue Timeout::Error
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def wait_for_servers
|
45
|
+
ports = SERVER_PIDS.keys
|
46
|
+
|
47
|
+
Timeout.timeout(10) do
|
48
|
+
ports.reject! { |port| port_open?(port) } while ports.any?
|
49
|
+
end
|
50
|
+
rescue Timeout::Error
|
51
|
+
raise "Could not reach ports: #{ports.join(", ")}"
|
52
|
+
end
|
53
|
+
|
54
|
+
Pathname.glob("sample_apps/*").select(&:directory?).each do |dir|
|
55
|
+
namespace :bench do
|
56
|
+
namespace dir.basename.to_s do
|
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
|
79
|
+
wait_for_servers
|
80
|
+
Dir.chdir("benchmarks") { sh "k6 run #{dir.basename}.js" }
|
81
|
+
ensure
|
82
|
+
stop_servers
|
83
|
+
end
|
84
|
+
|
85
|
+
task :boot_protected_app do
|
86
|
+
boot_server(dir, port: PORT_PROTECTED)
|
87
|
+
end
|
88
|
+
|
89
|
+
task :boot_unprotected_app do
|
90
|
+
boot_server(dir, port: PORT_UNPROTECTED, env: {"AIKIDO_DISABLED" => "true"})
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/tasklib/libzen.rake
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
require "open-uri"
|
5
|
+
require "rubygems/package_task"
|
6
|
+
|
7
|
+
require_relative "../lib/aikido/zen/version"
|
8
|
+
|
9
|
+
class LibZen
|
10
|
+
attr_reader :platform, :suffix, :artifact
|
11
|
+
|
12
|
+
def initialize(platform_suffix, artifact = nil)
|
13
|
+
platform, suffix = platform_suffix.split(".", 2)
|
14
|
+
@platform = Gem::Platform.new(platform)
|
15
|
+
@suffix = suffix
|
16
|
+
@artifact = artifact
|
17
|
+
end
|
18
|
+
|
19
|
+
def version
|
20
|
+
"v#{Aikido::Zen::LIBZEN_VERSION}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def path
|
24
|
+
"lib/aikido/zen/libzen-#{version}-#{platform}.#{suffix}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def url
|
28
|
+
File.join("https://github.com/AikidoSec/zen-internals/releases/download", version, artifact)
|
29
|
+
end
|
30
|
+
|
31
|
+
def gemspec(source = Bundler.load_gemspec("aikido-zen.gemspec"))
|
32
|
+
return @spec if defined?(@spec)
|
33
|
+
|
34
|
+
@spec = source.dup
|
35
|
+
@spec.platform = platform
|
36
|
+
@spec.files << path
|
37
|
+
@spec
|
38
|
+
end
|
39
|
+
|
40
|
+
def gem_path
|
41
|
+
"pkg/#{gemspec.name}-#{gemspec.version}-#{gemspec.platform}.gem"
|
42
|
+
end
|
43
|
+
|
44
|
+
def resolvable?
|
45
|
+
downloadable? || File.exist?(path)
|
46
|
+
end
|
47
|
+
|
48
|
+
def downloadable?
|
49
|
+
!artifact.nil?
|
50
|
+
end
|
51
|
+
|
52
|
+
def download
|
53
|
+
puts "Downloading #{path}"
|
54
|
+
File.open(path, "wb") { |file| FileUtils.copy_stream(URI(url).open("rb"), file) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def verify
|
58
|
+
expected = URI(url + ".sha256sum").read.split(/\s+/).first
|
59
|
+
actual = Digest::SHA256.file(path).to_s
|
60
|
+
|
61
|
+
if expected != actual
|
62
|
+
abort "Checksum verification failed for #{path}: expected #{expected}, but got #{actual}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def namespace
|
67
|
+
platform.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
def pkg_dir
|
71
|
+
File.dirname(gem_path)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
LIBZENS = [
|
76
|
+
LibZen.new("arm64-darwin.dylib", "libzen_internals_aarch64-apple-darwin.dylib"),
|
77
|
+
LibZen.new("arm64-linux.so", "libzen_internals_aarch64-unknown-linux-gnu.so"),
|
78
|
+
LibZen.new("arm64-linux-musl.so", "libzen_internals_aarch64-unknown-linux-musl.so"),
|
79
|
+
LibZen.new("aarch64-linux.so", "libzen_internals_aarch64-unknown-linux-gnu.so"),
|
80
|
+
LibZen.new("x86_64-darwin.dylib", "libzen_internals_x86_64-apple-darwin.dylib"),
|
81
|
+
LibZen.new("x86_64-linux.so", "libzen_internals_x86_64-unknown-linux-gnu.so"),
|
82
|
+
LibZen.new("x86_64-linux-musl.so", "libzen_internals_x86_64-unknown-linux-musl.so"),
|
83
|
+
LibZen.new("x86_64-mingw64.dll", "libzen_internals_x86_64-pc-windows-gnu.dll"),
|
84
|
+
# Not officially supported, but used during testing:
|
85
|
+
LibZen.new("x86_64-freebsd.so"),
|
86
|
+
LibZen.new("x86_64-solaris.so")
|
87
|
+
].filter(&:resolvable?)
|
88
|
+
|
89
|
+
namespace :libzen do
|
90
|
+
LIBZENS.each do |lib|
|
91
|
+
desc "Download libzen for #{lib.platform} if necessary"
|
92
|
+
task(lib.namespace => lib.path)
|
93
|
+
|
94
|
+
if lib.downloadable?
|
95
|
+
file(lib.path) do
|
96
|
+
lib.download
|
97
|
+
lib.verify
|
98
|
+
end
|
99
|
+
CLEAN.include(lib.path)
|
100
|
+
end
|
101
|
+
|
102
|
+
directory lib.pkg_dir
|
103
|
+
CLOBBER.include(lib.pkg_dir)
|
104
|
+
|
105
|
+
file(lib.gem_path => [lib.path, lib.pkg_dir]) do
|
106
|
+
path = Gem::Package.build(lib.gemspec)
|
107
|
+
mv path, lib.pkg_dir
|
108
|
+
end
|
109
|
+
CLOBBER.include(lib.pkg_dir)
|
110
|
+
|
111
|
+
task "#{lib.namespace}:release" => [lib.gem_path, "release:guard_clean"] do
|
112
|
+
sh "gem", "push", lib.gem_path
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
desc "Build all the native gems"
|
117
|
+
task gems: LIBZENS.map(&:gem_path)
|
118
|
+
|
119
|
+
desc "Push all the native gems to RubyGems"
|
120
|
+
task release: LIBZENS.map { |lib| "#{lib.namespace}:release" }
|
121
|
+
|
122
|
+
desc "Download the libzen pre-built library for all platforms"
|
123
|
+
task "download:all" => LIBZENS.map(&:path)
|
124
|
+
|
125
|
+
desc "Downloads the libzen library for the current platform"
|
126
|
+
task "download:current" do
|
127
|
+
platform = Gem::Platform.local.dup
|
128
|
+
platform.version = nil unless Rake::Task.task_defined?("libzen:#{platform}")
|
129
|
+
|
130
|
+
# Invoke the most specific task
|
131
|
+
Rake::Task["libzen:#{platform}"].invoke
|
132
|
+
end
|
133
|
+
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
|