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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.aikido +6 -0
  3. data/.ruby-version +1 -0
  4. data/.simplecov +26 -0
  5. data/.standard.yml +3 -0
  6. data/LICENSE +674 -0
  7. data/README.md +146 -0
  8. data/Rakefile +67 -0
  9. data/benchmarks/README.md +23 -0
  10. data/benchmarks/rails7.1_sql_injection.js +70 -0
  11. data/docs/banner.svg +202 -0
  12. data/docs/config.md +125 -0
  13. data/docs/proxy.md +10 -0
  14. data/docs/rails.md +114 -0
  15. data/lib/aikido/zen/actor.rb +116 -0
  16. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  17. data/lib/aikido/zen/agent.rb +179 -0
  18. data/lib/aikido/zen/api_client.rb +145 -0
  19. data/lib/aikido/zen/attack.rb +207 -0
  20. data/lib/aikido/zen/background_worker.rb +52 -0
  21. data/lib/aikido/zen/capped_collections.rb +68 -0
  22. data/lib/aikido/zen/collector/hosts.rb +15 -0
  23. data/lib/aikido/zen/collector/routes.rb +66 -0
  24. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  25. data/lib/aikido/zen/collector/stats.rb +111 -0
  26. data/lib/aikido/zen/collector/users.rb +30 -0
  27. data/lib/aikido/zen/collector.rb +144 -0
  28. data/lib/aikido/zen/config.rb +282 -0
  29. data/lib/aikido/zen/context/rack_request.rb +24 -0
  30. data/lib/aikido/zen/context/rails_request.rb +44 -0
  31. data/lib/aikido/zen/context.rb +112 -0
  32. data/lib/aikido/zen/detached_agent/agent.rb +78 -0
  33. data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
  34. data/lib/aikido/zen/detached_agent/server.rb +78 -0
  35. data/lib/aikido/zen/detached_agent.rb +2 -0
  36. data/lib/aikido/zen/errors.rb +107 -0
  37. data/lib/aikido/zen/event.rb +71 -0
  38. data/lib/aikido/zen/internals.rb +103 -0
  39. data/lib/aikido/zen/libzen-v0.1.39-aarch64-linux.so +0 -0
  40. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
  41. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  42. data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
  43. data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
  44. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  45. data/lib/aikido/zen/outbound_connection.rb +45 -0
  46. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  47. data/lib/aikido/zen/package.rb +22 -0
  48. data/lib/aikido/zen/payload.rb +50 -0
  49. data/lib/aikido/zen/rails_engine.rb +56 -0
  50. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  51. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  52. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  53. data/lib/aikido/zen/rate_limiter.rb +50 -0
  54. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  55. data/lib/aikido/zen/request/rails_router.rb +77 -0
  56. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  57. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  58. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  59. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  60. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  61. data/lib/aikido/zen/request/schema.rb +87 -0
  62. data/lib/aikido/zen/request.rb +122 -0
  63. data/lib/aikido/zen/route.rb +39 -0
  64. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  65. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  66. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  67. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  68. data/lib/aikido/zen/runtime_settings.rb +65 -0
  69. data/lib/aikido/zen/scan.rb +75 -0
  70. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  71. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
  72. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  73. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
  74. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -0
  75. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  76. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  77. data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
  78. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
  79. data/lib/aikido/zen/scanners.rb +7 -0
  80. data/lib/aikido/zen/sink.rb +118 -0
  81. data/lib/aikido/zen/sinks/action_controller.rb +83 -0
  82. data/lib/aikido/zen/sinks/async_http.rb +80 -0
  83. data/lib/aikido/zen/sinks/curb.rb +113 -0
  84. data/lib/aikido/zen/sinks/em_http.rb +83 -0
  85. data/lib/aikido/zen/sinks/excon.rb +118 -0
  86. data/lib/aikido/zen/sinks/file.rb +112 -0
  87. data/lib/aikido/zen/sinks/http.rb +93 -0
  88. data/lib/aikido/zen/sinks/httpclient.rb +95 -0
  89. data/lib/aikido/zen/sinks/httpx.rb +78 -0
  90. data/lib/aikido/zen/sinks/kernel.rb +33 -0
  91. data/lib/aikido/zen/sinks/mysql2.rb +31 -0
  92. data/lib/aikido/zen/sinks/net_http.rb +101 -0
  93. data/lib/aikido/zen/sinks/patron.rb +103 -0
  94. data/lib/aikido/zen/sinks/pg.rb +72 -0
  95. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  96. data/lib/aikido/zen/sinks/socket.rb +78 -0
  97. data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
  98. data/lib/aikido/zen/sinks/trilogy.rb +31 -0
  99. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  100. data/lib/aikido/zen/sinks.rb +36 -0
  101. data/lib/aikido/zen/sinks_dsl.rb +250 -0
  102. data/lib/aikido/zen/synchronizable.rb +24 -0
  103. data/lib/aikido/zen/system_info.rb +84 -0
  104. data/lib/aikido/zen/version.rb +10 -0
  105. data/lib/aikido/zen/worker.rb +87 -0
  106. data/lib/aikido/zen.rb +246 -0
  107. data/lib/aikido-zen.rb +3 -0
  108. data/placeholder/.gitignore +4 -0
  109. data/placeholder/README.md +11 -0
  110. data/placeholder/Rakefile +75 -0
  111. data/placeholder/lib/placeholder.rb.template +3 -0
  112. data/placeholder/placeholder.gemspec.template +20 -0
  113. data/tasklib/bench.rake +94 -0
  114. data/tasklib/libzen.rake +133 -0
  115. data/tasklib/wrk.rb +88 -0
  116. 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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "aikido/zen"
@@ -0,0 +1,4 @@
1
+ LICENSE
2
+ *.gemspec
3
+ /lib/*.rb
4
+ *.gem
@@ -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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ raise LoadError, "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."
@@ -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
@@ -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
@@ -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