aikido-zen 1.0.1.beta.2-x86_64-linux-musl

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 (115) 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/rails.md +70 -0
  14. data/lib/aikido/zen/actor.rb +116 -0
  15. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  16. data/lib/aikido/zen/agent.rb +179 -0
  17. data/lib/aikido/zen/api_client.rb +142 -0
  18. data/lib/aikido/zen/attack.rb +207 -0
  19. data/lib/aikido/zen/background_worker.rb +52 -0
  20. data/lib/aikido/zen/capped_collections.rb +68 -0
  21. data/lib/aikido/zen/collector/hosts.rb +15 -0
  22. data/lib/aikido/zen/collector/routes.rb +66 -0
  23. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  24. data/lib/aikido/zen/collector/stats.rb +111 -0
  25. data/lib/aikido/zen/collector/users.rb +30 -0
  26. data/lib/aikido/zen/collector.rb +144 -0
  27. data/lib/aikido/zen/config.rb +279 -0
  28. data/lib/aikido/zen/context/rack_request.rb +24 -0
  29. data/lib/aikido/zen/context/rails_request.rb +42 -0
  30. data/lib/aikido/zen/context.rb +112 -0
  31. data/lib/aikido/zen/detached_agent/agent.rb +78 -0
  32. data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
  33. data/lib/aikido/zen/detached_agent/server.rb +41 -0
  34. data/lib/aikido/zen/detached_agent.rb +2 -0
  35. data/lib/aikido/zen/errors.rb +107 -0
  36. data/lib/aikido/zen/event.rb +71 -0
  37. data/lib/aikido/zen/internals.rb +102 -0
  38. data/lib/aikido/zen/libzen-v0.1.39-x86_64-linux-musl.so +0 -0
  39. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
  40. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  41. data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
  42. data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
  43. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  44. data/lib/aikido/zen/outbound_connection.rb +45 -0
  45. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  46. data/lib/aikido/zen/package.rb +22 -0
  47. data/lib/aikido/zen/payload.rb +50 -0
  48. data/lib/aikido/zen/rails_engine.rb +70 -0
  49. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  50. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  51. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  52. data/lib/aikido/zen/rate_limiter.rb +50 -0
  53. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  54. data/lib/aikido/zen/request/rails_router.rb +72 -0
  55. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  56. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  57. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  58. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  59. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  60. data/lib/aikido/zen/request/schema.rb +87 -0
  61. data/lib/aikido/zen/request.rb +103 -0
  62. data/lib/aikido/zen/route.rb +39 -0
  63. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  64. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  65. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  66. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  67. data/lib/aikido/zen/runtime_settings.rb +65 -0
  68. data/lib/aikido/zen/scan.rb +75 -0
  69. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  70. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
  71. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  72. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
  73. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -0
  74. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  75. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  76. data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
  77. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
  78. data/lib/aikido/zen/scanners.rb +7 -0
  79. data/lib/aikido/zen/sink.rb +118 -0
  80. data/lib/aikido/zen/sinks/action_controller.rb +83 -0
  81. data/lib/aikido/zen/sinks/async_http.rb +82 -0
  82. data/lib/aikido/zen/sinks/curb.rb +115 -0
  83. data/lib/aikido/zen/sinks/em_http.rb +85 -0
  84. data/lib/aikido/zen/sinks/excon.rb +121 -0
  85. data/lib/aikido/zen/sinks/file.rb +116 -0
  86. data/lib/aikido/zen/sinks/http.rb +95 -0
  87. data/lib/aikido/zen/sinks/httpclient.rb +97 -0
  88. data/lib/aikido/zen/sinks/httpx.rb +80 -0
  89. data/lib/aikido/zen/sinks/kernel.rb +34 -0
  90. data/lib/aikido/zen/sinks/mysql2.rb +33 -0
  91. data/lib/aikido/zen/sinks/net_http.rb +103 -0
  92. data/lib/aikido/zen/sinks/patron.rb +105 -0
  93. data/lib/aikido/zen/sinks/pg.rb +74 -0
  94. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  95. data/lib/aikido/zen/sinks/socket.rb +80 -0
  96. data/lib/aikido/zen/sinks/sqlite3.rb +49 -0
  97. data/lib/aikido/zen/sinks/trilogy.rb +33 -0
  98. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  99. data/lib/aikido/zen/sinks.rb +39 -0
  100. data/lib/aikido/zen/sinks_dsl.rb +226 -0
  101. data/lib/aikido/zen/synchronizable.rb +24 -0
  102. data/lib/aikido/zen/system_info.rb +84 -0
  103. data/lib/aikido/zen/version.rb +10 -0
  104. data/lib/aikido/zen/worker.rb +87 -0
  105. data/lib/aikido/zen.rb +206 -0
  106. data/lib/aikido-zen.rb +3 -0
  107. data/placeholder/.gitignore +4 -0
  108. data/placeholder/README.md +11 -0
  109. data/placeholder/Rakefile +75 -0
  110. data/placeholder/lib/placeholder.rb.template +3 -0
  111. data/placeholder/placeholder.gemspec.template +20 -0
  112. data/tasklib/bench.rake +94 -0
  113. data/tasklib/libzen.rake +132 -0
  114. data/tasklib/wrk.rb +88 -0
  115. metadata +204 -0
data/lib/aikido/zen.rb ADDED
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ # IMPORTANT: Any files that load sinks or start the Aikido Agent should
4
+ # be required in `Aikido::Zen.protect!`.
5
+
6
+ require_relative "zen/version"
7
+ require_relative "zen/errors"
8
+ require_relative "zen/actor"
9
+ require_relative "zen/config"
10
+ require_relative "zen/collector"
11
+ require_relative "zen/system_info"
12
+ require_relative "zen/worker"
13
+ require_relative "zen/agent"
14
+ require_relative "zen/api_client"
15
+ require_relative "zen/context"
16
+ require_relative "zen/detached_agent"
17
+ require_relative "zen/middleware/check_allowed_addresses"
18
+ require_relative "zen/middleware/middleware"
19
+ require_relative "zen/middleware/request_tracker"
20
+ require_relative "zen/middleware/set_context"
21
+ require_relative "zen/outbound_connection"
22
+ require_relative "zen/outbound_connection_monitor"
23
+ require_relative "zen/runtime_settings"
24
+ require_relative "zen/rate_limiter"
25
+ require_relative "zen/scanners"
26
+
27
+ module Aikido
28
+ module Zen
29
+ # Enable protection. Until this method is called no sinks are loaded
30
+ # and the Aikido Agent does not start.
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
+ # IMPORTANT: Any files that load sinks or start the Aikido Agent
40
+ # should be required here only.
41
+ require_relative "zen/rails_engine" if defined?(::Rails)
42
+ end
43
+
44
+ # @return [Aikido::Zen::Config] the agent configuration.
45
+ def self.config
46
+ @config ||= Config.new
47
+ end
48
+
49
+ # @return [Aikido::Zen::RuntimeSettings] the firewall configuration sourced
50
+ # from your Aikido dashboard. This is periodically polled for updates.
51
+ def self.runtime_settings
52
+ @runtime_settings ||= RuntimeSettings.new
53
+ end
54
+
55
+ def self.runtime_settings=(settings)
56
+ @runtime_settings = settings
57
+ end
58
+
59
+ # Gets information about the current system configuration, which is sent to
60
+ # the server along with any events.
61
+ def self.system_info
62
+ @system_info ||= SystemInfo.new
63
+ end
64
+
65
+ # Manages runtime metrics extracted from your app, which are uploaded to the
66
+ # Aikido servers if configured to do so.
67
+ def self.collector
68
+ check_and_handle_fork
69
+ @collector ||= Collector.new
70
+ end
71
+
72
+ def self.detached_agent
73
+ check_and_handle_fork
74
+ @detached_agent ||= DetachedAgent::Agent.new
75
+ end
76
+
77
+ # Gets the current context object that holds all information about the
78
+ # current request.
79
+ #
80
+ # @return [Aikido::Zen::Context, nil]
81
+ def self.current_context
82
+ Thread.current[:_aikido_current_context_]
83
+ end
84
+
85
+ # Sets the current context object that holds all information about the
86
+ # current request, or +nil+ to clear the current context.
87
+ #
88
+ # @param context [Aikido::Zen::Context, nil]
89
+ # @return [Aikido::Zen::Context, nil]
90
+ def self.current_context=(context)
91
+ Thread.current[:_aikido_current_context_] = context
92
+ end
93
+
94
+ # Track statistics about an HTTP request the app is handling.
95
+ #
96
+ # @param request [Aikido::Zen::Request]
97
+ # @return [void]
98
+ def self.track_request(request)
99
+ collector.track_request
100
+ end
101
+
102
+ def self.track_discovered_route(request)
103
+ collector.track_route(request)
104
+ end
105
+
106
+ # Tracks a network connection made to an external service.
107
+ #
108
+ # @param connection [Aikido::Zen::OutboundConnection]
109
+ # @return [void]
110
+ def self.track_outbound(connection)
111
+ collector.track_outbound(connection)
112
+ end
113
+
114
+ # Track statistics about the result of a Sink's scan, and report it as
115
+ # an Attack if one is detected.
116
+ #
117
+ # @param scan [Aikido::Zen::Scan]
118
+ # @return [void]
119
+ # @raise [Aikido::Zen::UnderAttackError] if the scan detected an Attack
120
+ # and blocking_mode is enabled.
121
+ def self.track_scan(scan)
122
+ collector.track_scan(scan)
123
+ agent.handle_attack(scan.attack) if scan.attack?
124
+ end
125
+
126
+ # Track the user making the current request.
127
+ #
128
+ # @param (see Aikido::Zen.Actor)
129
+ # @return [void]
130
+ def self.track_user(user)
131
+ return if config.disabled?
132
+
133
+ if (actor = Aikido::Zen::Actor(user))
134
+ collector.track_user(actor)
135
+ current_context.request.actor = actor if current_context
136
+ else
137
+ config.logger.warn(format(<<~LOG, obj: user))
138
+ Incompatible object sent to track_user: %<obj>p
139
+
140
+ The object must either implement #to_aikido_actor, or be a Hash with
141
+ an :id (or "id") and, optionally, a :name (or "name") key.
142
+ LOG
143
+ end
144
+ end
145
+
146
+ # Marks that the Zen middleware was installed properly
147
+ # @return void
148
+ def self.middleware_installed!
149
+ collector.middleware_installed!
150
+ end
151
+
152
+ # Load all sinks matching libraries loaded into memory. This method should
153
+ # be called after all other dependencies have been loaded into memory (i.e.
154
+ # at the end of the initialization process).
155
+ #
156
+ # If a new gem is required, this method can be called again safely.
157
+ #
158
+ # @return [void]
159
+ def self.load_sinks!
160
+ require_relative "zen/sinks"
161
+ end
162
+
163
+ # @!visibility private
164
+ # Stop any background threads.
165
+ def self.stop!
166
+ @agent&.stop!
167
+ @detached_agent_server&.stop!
168
+ end
169
+
170
+ # @!visibility private
171
+ # Starts the background agent if it has not been started yet.
172
+ def self.agent
173
+ @agent ||= Agent.start
174
+ end
175
+
176
+ def self.detached_agent_server
177
+ @detached_agent_server ||= DetachedAgent::Server.start!
178
+ end
179
+
180
+ class << self
181
+ # `agent` and `detached_agent` are started on the first method call.
182
+ # A mutex controls thread execution to prevent multiple attempts.
183
+ LOCK = Mutex.new
184
+
185
+ def start!
186
+ @pid = Process.pid
187
+ LOCK.synchronize do
188
+ agent
189
+ detached_agent_server
190
+ end
191
+ end
192
+
193
+ def check_and_handle_fork
194
+ if has_forked
195
+ @detached_agent&.handle_fork
196
+ end
197
+ end
198
+
199
+ def has_forked
200
+ pid_changed = Process.pid != @pid
201
+ @pid = Process.pid
202
+ pid_changed
203
+ end
204
+ end
205
+ end
206
+ 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,132 @@
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("x86_64-darwin.dylib", "libzen_internals_x86_64-apple-darwin.dylib"),
80
+ LibZen.new("x86_64-linux.so", "libzen_internals_x86_64-unknown-linux-gnu.so"),
81
+ LibZen.new("x86_64-linux-musl.so", "libzen_internals_x86_64-unknown-linux-musl.so"),
82
+ LibZen.new("x86_64-mingw64.dll", "libzen_internals_x86_64-pc-windows-gnu.dll"),
83
+ # Not officially supported, but used during testing:
84
+ LibZen.new("x86_64-freebsd.so"),
85
+ LibZen.new("x86_64-solaris.so")
86
+ ].filter(&:resolvable?)
87
+
88
+ namespace :libzen do
89
+ LIBZENS.each do |lib|
90
+ desc "Download libzen for #{lib.platform} if necessary"
91
+ task(lib.namespace => lib.path)
92
+
93
+ if lib.downloadable?
94
+ file(lib.path) do
95
+ lib.download
96
+ lib.verify
97
+ end
98
+ CLEAN.include(lib.path)
99
+ end
100
+
101
+ directory lib.pkg_dir
102
+ CLOBBER.include(lib.pkg_dir)
103
+
104
+ file(lib.gem_path => [lib.path, lib.pkg_dir]) do
105
+ path = Gem::Package.build(lib.gemspec)
106
+ mv path, lib.pkg_dir
107
+ end
108
+ CLOBBER.include(lib.pkg_dir)
109
+
110
+ task "#{lib.namespace}:release" => [lib.gem_path, "release:guard_clean"] do
111
+ sh "gem", "push", lib.gem_path
112
+ end
113
+ end
114
+
115
+ desc "Build all the native gems"
116
+ task gems: LIBZENS.map(&:gem_path)
117
+
118
+ desc "Push all the native gems to RubyGems"
119
+ task release: LIBZENS.map { |lib| "#{lib.namespace}:release" }
120
+
121
+ desc "Download the libzen pre-built library for all platforms"
122
+ task "download:all" => LIBZENS.map(&:path)
123
+
124
+ desc "Downloads the libzen library for the current platform"
125
+ task "download:current" do
126
+ platform = Gem::Platform.local.dup
127
+ platform.version = nil unless Rake::Task.task_defined?("libzen:#{platform}")
128
+
129
+ # Invoke the most specific task
130
+ Rake::Task["libzen:#{platform}"].invoke
131
+ end
132
+ 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