aikido-zen 1.0.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 (125) hide show
  1. checksums.yaml +7 -0
  2. data/.aikido +6 -0
  3. data/.ruby-version +1 -0
  4. data/.simplecov +32 -0
  5. data/.standard.yml +3 -0
  6. data/LICENSE +674 -0
  7. data/README.md +148 -0
  8. data/Rakefile +67 -0
  9. data/benchmarks/README.md +22 -0
  10. data/benchmarks/rails7.1_benchmark.js +1 -0
  11. data/benchmarks/rails7.1_sql_injection.js +102 -0
  12. data/docs/banner.svg +202 -0
  13. data/docs/config.md +133 -0
  14. data/docs/proxy.md +10 -0
  15. data/docs/rails.md +112 -0
  16. data/docs/troubleshooting.md +62 -0
  17. data/lib/aikido/zen/actor.rb +146 -0
  18. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  19. data/lib/aikido/zen/agent.rb +181 -0
  20. data/lib/aikido/zen/api_client.rb +145 -0
  21. data/lib/aikido/zen/attack.rb +217 -0
  22. data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
  23. data/lib/aikido/zen/attack_wave.rb +88 -0
  24. data/lib/aikido/zen/background_worker.rb +52 -0
  25. data/lib/aikido/zen/cache.rb +91 -0
  26. data/lib/aikido/zen/capped_collections.rb +86 -0
  27. data/lib/aikido/zen/collector/event.rb +238 -0
  28. data/lib/aikido/zen/collector/hosts.rb +30 -0
  29. data/lib/aikido/zen/collector/routes.rb +71 -0
  30. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  31. data/lib/aikido/zen/collector/stats.rb +122 -0
  32. data/lib/aikido/zen/collector/users.rb +32 -0
  33. data/lib/aikido/zen/collector.rb +223 -0
  34. data/lib/aikido/zen/config.rb +312 -0
  35. data/lib/aikido/zen/context/rack_request.rb +27 -0
  36. data/lib/aikido/zen/context/rails_request.rb +47 -0
  37. data/lib/aikido/zen/context.rb +145 -0
  38. data/lib/aikido/zen/detached_agent/agent.rb +79 -0
  39. data/lib/aikido/zen/detached_agent/front_object.rb +41 -0
  40. data/lib/aikido/zen/detached_agent/server.rb +78 -0
  41. data/lib/aikido/zen/detached_agent.rb +2 -0
  42. data/lib/aikido/zen/errors.rb +107 -0
  43. data/lib/aikido/zen/event.rb +116 -0
  44. data/lib/aikido/zen/helpers.rb +24 -0
  45. data/lib/aikido/zen/internals.rb +123 -0
  46. data/lib/aikido/zen/libzen-v0.1.48-aarch64-linux.so +0 -0
  47. data/lib/aikido/zen/middleware/allowed_address_checker.rb +26 -0
  48. data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
  49. data/lib/aikido/zen/middleware/context_setter.rb +26 -0
  50. data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
  51. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  52. data/lib/aikido/zen/middleware/rack_throttler.rb +50 -0
  53. data/lib/aikido/zen/middleware/request_tracker.rb +197 -0
  54. data/lib/aikido/zen/outbound_connection.rb +62 -0
  55. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  56. data/lib/aikido/zen/package.rb +22 -0
  57. data/lib/aikido/zen/payload.rb +50 -0
  58. data/lib/aikido/zen/rails_engine.rb +53 -0
  59. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  60. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  61. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  62. data/lib/aikido/zen/rate_limiter.rb +50 -0
  63. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  64. data/lib/aikido/zen/request/rails_router.rb +92 -0
  65. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  66. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  67. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  68. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  69. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  70. data/lib/aikido/zen/request/schema.rb +87 -0
  71. data/lib/aikido/zen/request.rb +88 -0
  72. data/lib/aikido/zen/route.rb +96 -0
  73. data/lib/aikido/zen/runtime_settings/endpoints.rb +78 -0
  74. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  75. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  76. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  77. data/lib/aikido/zen/runtime_settings.rb +66 -0
  78. data/lib/aikido/zen/scan.rb +75 -0
  79. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +68 -0
  80. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +64 -0
  81. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  82. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +65 -0
  83. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +94 -0
  84. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  85. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  86. data/lib/aikido/zen/scanners/ssrf_scanner.rb +266 -0
  87. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +55 -0
  88. data/lib/aikido/zen/scanners.rb +7 -0
  89. data/lib/aikido/zen/sink.rb +118 -0
  90. data/lib/aikido/zen/sinks/action_controller.rb +85 -0
  91. data/lib/aikido/zen/sinks/async_http.rb +80 -0
  92. data/lib/aikido/zen/sinks/curb.rb +113 -0
  93. data/lib/aikido/zen/sinks/em_http.rb +83 -0
  94. data/lib/aikido/zen/sinks/excon.rb +118 -0
  95. data/lib/aikido/zen/sinks/file.rb +153 -0
  96. data/lib/aikido/zen/sinks/http.rb +93 -0
  97. data/lib/aikido/zen/sinks/httpclient.rb +95 -0
  98. data/lib/aikido/zen/sinks/httpx.rb +78 -0
  99. data/lib/aikido/zen/sinks/kernel.rb +33 -0
  100. data/lib/aikido/zen/sinks/mysql2.rb +31 -0
  101. data/lib/aikido/zen/sinks/net_http.rb +101 -0
  102. data/lib/aikido/zen/sinks/patron.rb +103 -0
  103. data/lib/aikido/zen/sinks/pg.rb +72 -0
  104. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  105. data/lib/aikido/zen/sinks/socket.rb +85 -0
  106. data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
  107. data/lib/aikido/zen/sinks/trilogy.rb +31 -0
  108. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  109. data/lib/aikido/zen/sinks.rb +36 -0
  110. data/lib/aikido/zen/sinks_dsl.rb +250 -0
  111. data/lib/aikido/zen/synchronizable.rb +24 -0
  112. data/lib/aikido/zen/system_info.rb +80 -0
  113. data/lib/aikido/zen/version.rb +10 -0
  114. data/lib/aikido/zen/worker.rb +87 -0
  115. data/lib/aikido/zen.rb +303 -0
  116. data/lib/aikido-zen.rb +3 -0
  117. data/placeholder/.gitignore +4 -0
  118. data/placeholder/README.md +11 -0
  119. data/placeholder/Rakefile +75 -0
  120. data/placeholder/lib/placeholder.rb.template +3 -0
  121. data/placeholder/placeholder.gemspec.template +20 -0
  122. data/tasklib/bench.rake +94 -0
  123. data/tasklib/libzen.rake +133 -0
  124. data/tasklib/wrk.rb +88 -0
  125. metadata +214 -0
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "json"
5
+ require "logger"
6
+
7
+ require_relative "context"
8
+
9
+ module Aikido::Zen
10
+ class Config
11
+ # @return [Boolean] whether Aikido should be turned completely off (no
12
+ # intercepting calls to protect the app, no agent process running, no
13
+ # middleware installed). Defaults to false (so, enabled). Can be set
14
+ # via the AIKIDO_DISABLE environment variable.
15
+ attr_accessor :disabled
16
+ alias_method :disabled?, :disabled
17
+
18
+ # @return [Boolean] whether Aikido should only report infractions or block
19
+ # the request by raising an Exception. Defaults to whether AIKIDO_BLOCK
20
+ # is set to a non-empty value in your environment, or +false+ otherwise.
21
+ attr_accessor :blocking_mode
22
+ alias_method :blocking_mode?, :blocking_mode
23
+
24
+ # @return [URI] The HTTP host for the Aikido API. Defaults to
25
+ # +https://guard.aikido.dev+.
26
+ attr_reader :api_endpoint
27
+
28
+ # @return [URI] The HTTP host for the Aikido Runtime API. Defaults to
29
+ # +https://runtime.aikido.dev+.
30
+ attr_reader :realtime_endpoint
31
+
32
+ # @return [Hash] HTTP timeouts for communicating with the API.
33
+ attr_reader :api_timeouts
34
+
35
+ # @return [String] the token obtained when configuring the Firewall in the
36
+ # Aikido interface.
37
+ attr_accessor :api_token
38
+
39
+ # @return [Integer] the interval in seconds to poll the runtime API for
40
+ # settings changes. Defaults to evey 60 seconds.
41
+ attr_accessor :polling_interval
42
+
43
+ # @return [Array<Integer>] the delays in seconds to wait before sending
44
+ # each initial heartbeat event.
45
+ attr_accessor :initial_heartbeat_delays
46
+
47
+ # @return [#call] Callable that can be passed an Object and returns a String
48
+ # of JSON. Defaults to the standard library's JSON.dump method.
49
+ attr_accessor :json_encoder
50
+
51
+ # @return [#call] Callable that can be passed a JSON string and parses it
52
+ # into an Object. Defaults to the standard library's JSON.parse method.
53
+ attr_accessor :json_decoder
54
+
55
+ # @return [Logger]
56
+ attr_reader :logger
57
+
58
+ # @return [String] Path of the socket where the detached agent will listen.
59
+ # By default, is stored under the root application path with file name
60
+ # `aikido-detached-agent.sock`
61
+ attr_accessor :detached_agent_socket_path
62
+
63
+ # @return [Boolean] is the agent in debugging mode?
64
+ attr_accessor :debugging
65
+ alias_method :debugging?, :debugging
66
+
67
+ # @return [String] environment specific HTTP header providing the client IP.
68
+ attr_accessor :client_ip_header
69
+
70
+ # @return [Integer] maximum number of timing measurements to keep in memory
71
+ # before compressing them.
72
+ attr_accessor :max_performance_samples
73
+
74
+ # @return [Integer] maximum number of compressed performance samples to keep
75
+ # in memory. If we take more than this before reporting them to Aikido, we
76
+ # will discard the oldest samples.
77
+ attr_accessor :max_compressed_stats
78
+
79
+ # @return [Integer] maximum number of connections to outbound hosts to keep
80
+ # in memory in order to report them in the next heartbeat event. If new
81
+ # connections are added to the set before reporting them to Aikido, we
82
+ # will discard the oldest data point.
83
+ attr_accessor :max_outbound_connections
84
+
85
+ # @return [Integer] maximum number of users tracked via Zen.track_user to
86
+ # share with the Aikido servers on the next heartbeat event. If more
87
+ # unique users (by their ID) are tracked than this number, we will discard
88
+ # the oldest seen users.
89
+ attr_accessor :max_users_tracked
90
+
91
+ # @return [Proc{(Aikido::Zen::Request, Symbol) => Array(Integer, Hash, #each)}]
92
+ # Rack handler used to respond to requests from IPs, users or others blocked in the Aikido
93
+ # dashboard.
94
+ attr_accessor :blocked_responder
95
+
96
+ # @return [Proc{Aikido::Zen::Request => Array(Integer, Hash, #each)}]
97
+ # Rack handler used to respond to requests that have been rate limited.
98
+ attr_accessor :rate_limited_responder
99
+
100
+ # @return [Proc{Aikido::Zen::Request => String}] a proc that reads
101
+ # information off the current request and returns a String to
102
+ # differentiate different clients. By default this uses the request IP.
103
+ attr_accessor :rate_limiting_discriminator
104
+
105
+ # @return [Boolean] whether Aikido Zen should collect api schemas.
106
+ # Defaults to true. Can be set through AIKIDO_FEATURE_COLLECT_API_SCHEMA
107
+ # environment variable.
108
+ attr_accessor :collect_api_schema
109
+ alias_method :collect_api_schema?, :collect_api_schema
110
+
111
+ # @return [Integer] max number of requests we sample per endpoint when
112
+ # computing the schema.
113
+ attr_accessor :api_schema_max_samples
114
+
115
+ # @api private
116
+ # @return [Integer] max number of levels deep we want to read a nested
117
+ # strcture for performance reasons.
118
+ attr_accessor :api_schema_collection_max_depth
119
+
120
+ # @api private
121
+ # @return [Integer] max number of properties that we want to inspect per
122
+ # level of the structure for performance reasons.
123
+ attr_accessor :api_schema_collection_max_properties
124
+
125
+ # @api private
126
+ # @return [Proc<Hash => Aikido::Zen::Context>] callable that takes a
127
+ # Rack-compatible env Hash and returns a Context object with an HTTP
128
+ # request. This is meant to be overridden by each framework adapter.
129
+ attr_accessor :request_builder
130
+
131
+ # @api private
132
+ # @return [Integer] number of seconds to perform client-side rate limiting
133
+ # of events sent to the server.
134
+ attr_accessor :client_rate_limit_period
135
+
136
+ # @api private
137
+ # @return [Integer] max number of events sent during a sliding
138
+ # {client_rate_limit_period} window.
139
+ attr_accessor :client_rate_limit_max_events
140
+
141
+ # @api private
142
+ # @return [Integer] number of seconds to wait before sending an event after
143
+ # the server returns a 429 response.
144
+ attr_accessor :server_rate_limit_deadline
145
+
146
+ # @return [Boolean] whether Aikido Zen should scan for stored SSSRF attacks.
147
+ # Defaults to true. Can be set through AIKIDO_FEATURE_STORED_SSRF
148
+ # environment variable.
149
+ attr_accessor :stored_ssrf
150
+ alias_method :stored_ssrf?, :stored_ssrf
151
+
152
+ # @return [Array<String>] when checking for stored SSRF attacks, we want to
153
+ # allow known hosts that should be able to resolve to the IMDS service.
154
+ attr_accessor :imds_allowed_hosts
155
+
156
+ # @return [Boolean] whether Aikido Zen should harden methods where possible.
157
+ # Defaults to true. Can be set through AIKIDO_HARDEN environment variable.
158
+ attr_accessor :harden
159
+ alias_method :harden?, :harden
160
+
161
+ # @return [Integer] how many suspicious requests are allowed before an
162
+ # attack wave detected event is reported.
163
+ # Defaults to 15 requests.
164
+ attr_accessor :attack_wave_threshold
165
+
166
+ # @return [Integer] the minimum time in milliseconds between requests for
167
+ # requests to be part of an attack wave.
168
+ # Defaults to 1 minute in milliseconds.
169
+ attr_accessor :attack_wave_min_time_between_requests
170
+
171
+ # @return [Integer] the minimum time in milliseconds between reporting
172
+ # attack wave events.
173
+ # Defaults to 20 minutes in milliseconds.
174
+ attr_accessor :attack_wave_min_time_between_events
175
+
176
+ # @return [Integer] the maximum number of entries in the LRU cache.
177
+ # Defaults to 10,000 entries.
178
+ attr_accessor :attack_wave_max_cache_entries
179
+
180
+ def initialize
181
+ self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
182
+ self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
183
+ self.api_timeouts = 10
184
+ self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT", DEFAULT_AIKIDO_ENDPOINT)
185
+ self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
186
+ self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
187
+ self.polling_interval = 60 # 1 min
188
+ self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
189
+ self.json_encoder = DEFAULT_JSON_ENCODER
190
+ self.json_decoder = DEFAULT_JSON_DECODER
191
+ self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
192
+ self.logger = Logger.new($stdout, progname: "aikido", level: debugging ? Logger::DEBUG : Logger::INFO)
193
+ self.detached_agent_socket_path = ENV.fetch("AIKIDO_DETACHED_AGENT_SOCKET_PATH", DEFAULT_DETACHED_AGENT_SOCKET_PATH)
194
+ self.client_ip_header = ENV.fetch("AIKIDO_CLIENT_IP_HEADER", nil)
195
+ self.max_performance_samples = 5000
196
+ self.max_compressed_stats = 100
197
+ self.max_outbound_connections = 200
198
+ self.max_users_tracked = 1000
199
+ self.request_builder = Aikido::Zen::Context::RACK_REQUEST_BUILDER
200
+ self.blocked_responder = DEFAULT_BLOCKED_RESPONDER
201
+ self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
202
+ self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
203
+ self.server_rate_limit_deadline = 30 * 60 # 30 min
204
+ self.client_rate_limit_period = 60 * 60 # 1 hour
205
+ self.client_rate_limit_max_events = 100
206
+ self.collect_api_schema = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", true))
207
+ self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
208
+ self.api_schema_collection_max_depth = 20
209
+ self.api_schema_collection_max_properties = 20
210
+ self.stored_ssrf = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_STORED_SSRF", true))
211
+ self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
212
+ self.harden = read_boolean_from_env(ENV.fetch("AIKIDO_HARDEN", true))
213
+ self.attack_wave_threshold = 15
214
+ self.attack_wave_min_time_between_requests = 60 * 1000 # 1 min (ms)
215
+ self.attack_wave_min_time_between_events = 20 * 60 * 1000 # 20 min (ms)
216
+ self.attack_wave_max_cache_entries = 10_000
217
+ end
218
+
219
+ # Set the base URL for API requests.
220
+ #
221
+ # @param url [String, URI]
222
+ def api_endpoint=(url)
223
+ @api_endpoint = URI(url)
224
+ end
225
+
226
+ # Set the base URL for runtime API requests.
227
+ #
228
+ # @param url [String, URI]
229
+ def realtime_endpoint=(url)
230
+ @realtime_endpoint = URI(url)
231
+ end
232
+
233
+ # Set the logger and configure its severity level according to agent's debug mode
234
+ # @param logger [::Logger]
235
+ def logger=(logger)
236
+ @logger = logger
237
+ @logger.level = Logger::DEBUG if debugging
238
+ end
239
+
240
+ # @overload def api_timeouts=(timeouts)
241
+ # Configure granular connection timeouts for the Aikido Zen API. You
242
+ # can set any of these per call.
243
+ # @param timeouts [Hash]
244
+ # @option timeouts [Integer] :open_timeout Duration in seconds.
245
+ # @option timeouts [Integer] :read_timeout Duration in seconds.
246
+ # @option timeouts [Integer] :write_timeout Duration in seconds.
247
+ #
248
+ # @overload def api_timeouts=(duration)
249
+ # Configure the connection timeouts for the Aikido Zen API.
250
+ # @param duration [Integer] Duration in seconds to set for all three
251
+ # timeouts (open, read, and write).
252
+ def api_timeouts=(value)
253
+ value = {open_timeout: value, read_timeout: value, write_timeout: value} if value.respond_to?(:to_int)
254
+
255
+ @api_timeouts ||= {}
256
+ @api_timeouts.update(value)
257
+ end
258
+
259
+ def detached_agent_socket_uri
260
+ "drbunix:" + @detached_agent_socket_path
261
+ end
262
+
263
+ private
264
+
265
+ def read_boolean_from_env(value)
266
+ return value unless value.respond_to?(:to_str)
267
+
268
+ case value.to_str.strip
269
+ when "false", "", "0", "f"
270
+ false
271
+ else
272
+ true
273
+ end
274
+ end
275
+
276
+ # @!visibility private
277
+ DEFAULT_AIKIDO_ENDPOINT = "https://guard.aikido.dev"
278
+
279
+ # @!visibility private
280
+ DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
281
+
282
+ # @!visibility private
283
+ DEFAULT_JSON_ENCODER = JSON.method(:dump)
284
+
285
+ # @!visibility private
286
+ DEFAULT_JSON_DECODER = JSON.method(:parse)
287
+
288
+ # @!visibility private
289
+ DEFAULT_DETACHED_AGENT_SOCKET_PATH = "aikido-detached-agent.sock"
290
+
291
+ # @!visibility private
292
+ DEFAULT_BLOCKED_RESPONDER = ->(request, blocking_type) do
293
+ message = case blocking_type
294
+ when :ip
295
+ format("Your IP address is not allowed to access this resource. (Your IP: %s)", request.ip)
296
+ else
297
+ "You are blocked by Zen."
298
+ end
299
+ [403, {"Content-Type" => "text/plain"}, [message]]
300
+ end
301
+
302
+ # @!visibility private
303
+ DEFAULT_RATE_LIMITED_RESPONDER = ->(request) do
304
+ [429, {"Content-Type" => "text/plain"}, ["Too many requests."]]
305
+ end
306
+
307
+ # @!visibility private
308
+ DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
309
+ request.actor ? "actor:#{request.actor.id}" : request.ip
310
+ }
311
+ end
312
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../request"
4
+ require_relative "../request/heuristic_router"
5
+
6
+ module Aikido::Zen
7
+ # @!visibility private
8
+ Context::RACK_REQUEST_BUILDER = ->(env) do
9
+ # Normalize PATH_INFO so routes are correctly recognized in middleware.
10
+ env["PATH_INFO"] = Helpers.normalize_path(env["PATH_INFO"])
11
+
12
+ delegate = Rack::Request.new(env)
13
+ router = Aikido::Zen::Request::HeuristicRouter.new
14
+ request = Aikido::Zen::Request.new(delegate, framework: "rack", router: router)
15
+
16
+ Context.new(request) do |req|
17
+ {
18
+ query: req.GET,
19
+ body: req.POST,
20
+ route: {},
21
+ header: req.normalized_headers,
22
+ cookie: req.cookies,
23
+ subdomain: []
24
+ }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../request"
4
+ require_relative "../request/rails_router"
5
+
6
+ module Aikido::Zen
7
+ module Rails
8
+ def self.router
9
+ @router ||= Request::RailsRouter.new(::Rails.application.routes)
10
+ end
11
+ end
12
+
13
+ # @!visibility private
14
+ Context::RAILS_REQUEST_BUILDER = ->(env) do
15
+ # Normalize PATH_INFO so routes are correctly recognized in middleware.
16
+ env["PATH_INFO"] = Helpers.normalize_path(env["PATH_INFO"])
17
+
18
+ # Duplicate the Rack environment to prevent unexpected modifications from
19
+ # breaking Rails routing.
20
+ delegate = ActionDispatch::Request.new(env.dup)
21
+ request = Aikido::Zen::Request.new(
22
+ delegate, framework: "rails", router: Rails.router
23
+ )
24
+
25
+ decrypt_cookies = ->(req) do
26
+ return req.cookies unless req.respond_to?(:cookie_jar)
27
+
28
+ req.cookie_jar.map { |key, value|
29
+ plain_text = req.cookie_jar.encrypted[key].presence ||
30
+ req.cookie_jar.signed[key].presence ||
31
+ value
32
+ [key, plain_text]
33
+ }.to_h
34
+ end
35
+
36
+ Context.new(request) do |req|
37
+ {
38
+ query: req.query_parameters,
39
+ body: req.request_parameters,
40
+ route: req.path_parameters,
41
+ header: req.normalized_headers,
42
+ cookie: decrypt_cookies.call(req),
43
+ subdomain: req.subdomains
44
+ }
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ require_relative "request"
6
+ require_relative "payload"
7
+
8
+ module Aikido::Zen
9
+ class Context
10
+ # Build a Context object for the current HTTP request based on the currently
11
+ # configured request builder.
12
+ #
13
+ # @param env [Hash] the Rack env hash.
14
+ # @param config [Aikido::Zen::Config]
15
+ # @return [Aikido::Zen::Context]
16
+ def self.from_rack_env(env, config = Aikido::Zen.config)
17
+ config.request_builder.call(env)
18
+ end
19
+
20
+ # @return [Aikido::Zen::Request]
21
+ attr_reader :request
22
+
23
+ # @return [Boolean]
24
+ attr_accessor :scanning
25
+
26
+ # @param request [Rack::Request] a Request object that implements the
27
+ # Rack::Request API, to which we will delegate behavior.
28
+ # @param settings [Aikido::Zen::RuntimeSettings]
29
+ #
30
+ # @yieldparam request [Rack::Request] the given request object.
31
+ # @yieldreturn [Hash<Symbol, #flat_map>] map of payload source types
32
+ # to the actual data from the request to populate them.
33
+ def initialize(request, settings: Aikido::Zen.runtime_settings, &sources)
34
+ @request = request
35
+ @settings = settings
36
+ @payload_sources = sources
37
+ @metadata = {}
38
+ @scanning = false
39
+ end
40
+
41
+ # Fetch some metadata stored in the Context.
42
+ #
43
+ # @param key [String]
44
+ # @return [Object, nil]
45
+ def [](key)
46
+ @metadata[key]
47
+ end
48
+
49
+ # Store some metadata in the Context so other Scanners can use it.
50
+ #
51
+ # @param key [String]
52
+ # @param value [Object]
53
+ # @return [void]
54
+ def []=(key, value)
55
+ @metadata[key] = value
56
+ end
57
+
58
+ # Overrides the current request, and invalidates any memoized data obtained
59
+ # from it. This is useful for scenarios where setting the request in the
60
+ # middleware isn't enough, such as Rails, where the router modifies it after
61
+ # the middleware has seen it.
62
+ #
63
+ # @param new_request [Rack::Request]
64
+ # @return [void]
65
+ def update_request(new_request)
66
+ @payloads = nil
67
+ request.__setobj__(new_request)
68
+ end
69
+
70
+ # @return [Array<Aikido::Zen::Payload>] list of user inputs from all the
71
+ # different sources we recognize.
72
+ def payloads
73
+ @payloads ||= payload_sources.flat_map do |source, data|
74
+ extract_payloads_from(data, source)
75
+ end
76
+ end
77
+
78
+ # @return [Boolean] whether attack protection for the currently requested
79
+ # endpoint was disabled on the Aikido dashboard, or if the source IP for
80
+ # this request is in the "Bypass List".
81
+ def protection_disabled?
82
+ return false if request.nil?
83
+
84
+ !@settings.endpoints.match(request.route).all?(&:protected?) ||
85
+ @settings.allowed_ips.include?(request.ip)
86
+ end
87
+
88
+ # @!visibility private
89
+ def payload_sources
90
+ @payload_sources.call(request)
91
+ end
92
+
93
+ private
94
+
95
+ # @!visibility private
96
+ def extract_payloads_from(data, source_type, prefix = nil)
97
+ if data.respond_to?(:to_hash)
98
+ data.to_hash.flat_map do |key, value|
99
+ extract_payloads_from(value, source_type, [prefix, key].compact.join("."))
100
+ end
101
+ elsif data.respond_to?(:to_ary)
102
+ array = data.to_ary
103
+ return array if array.empty?
104
+
105
+ payloads = array.flat_map.with_index do |value, index|
106
+ extract_payloads_from(value, source_type, [prefix, index].compact.join("."))
107
+ end
108
+
109
+ unless Aikido::Zen.config.harden?
110
+ # Special case for File.join given a possibly nested array of strings,
111
+ # as might occur when a query parameter is an array.
112
+ begin
113
+ string = File.join__internal_for_aikido_zen(*array)
114
+ if unsafe_path?(string)
115
+ payloads << Payload.new(string, source_type, [prefix, "__File.join__"].compact.join("."))
116
+ end
117
+ rescue
118
+ # Could not create special payload for File.join.
119
+ end
120
+ end
121
+
122
+ payloads
123
+ else
124
+ [Payload.new(data, source_type, prefix.to_s)]
125
+ end
126
+ end
127
+
128
+ def unsafe_path?(filepath)
129
+ normalized_filepath = Pathname.new(filepath).cleanpath.to_s.downcase
130
+
131
+ Scanners::PathTraversal::DANGEROUS_PATH_PARTS.each do |dangerous_path_part|
132
+ return true if normalized_filepath.include?(dangerous_path_part)
133
+ end
134
+
135
+ Scanners::PathTraversal::DANGEROUS_PATH_STARTS.each do |dangerous_path_start|
136
+ return true if normalized_filepath.start_with?(dangerous_path_start)
137
+ end
138
+
139
+ false
140
+ end
141
+ end
142
+ end
143
+
144
+ require_relative "context/rack_request"
145
+ require_relative "context/rails_request"
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "drb/drb"
4
+ require "drb/unix"
5
+ require_relative "front_object"
6
+ require_relative "../background_worker"
7
+
8
+ module Aikido::Zen::DetachedAgent
9
+ # Agent that runs in forked processes. It communicates with the parent process to dRB
10
+ # calls. It's in charge of schedule and send heartbeats to the *parent process*, to be
11
+ # later pushed.
12
+ #
13
+ # heartbeat & polling interval are configured to 10s , because they are connecting with
14
+ # parent process. We want to have the freshest data.
15
+ #
16
+ # It's possible to use `extend Forwardable` here for one-line forward calls to the
17
+ # @front_object object. Unfortunately, the methods to be called are
18
+ # created at runtime by `DRbObject`, which leads to an ugly warning about
19
+ # private methods after the delegator is bound.
20
+ class Agent
21
+ attr_reader :worker
22
+
23
+ def initialize(
24
+ config: Aikido::Zen.config,
25
+ worker: Aikido::Zen::Worker.new(config: config),
26
+ heartbeat_interval: 10,
27
+ polling_interval: 10,
28
+ collector: Aikido::Zen.collector
29
+ )
30
+ @config = config
31
+ @worker = worker
32
+ @heartbeat_interval = heartbeat_interval
33
+ @polling_interval = polling_interval
34
+
35
+ @collector = collector
36
+
37
+ @front_object = DRbObject.new_with_uri(config.detached_agent_socket_uri)
38
+
39
+ @has_forked = false
40
+ schedule_tasks
41
+ end
42
+
43
+ def send_collector_events
44
+ events_data = @collector.flush_events.map(&:as_json)
45
+ @front_object.send_collector_events(events_data)
46
+ end
47
+
48
+ def calculate_rate_limits(request)
49
+ @front_object.calculate_rate_limits(request.route.as_json, request.ip, request.actor.as_json)
50
+ end
51
+
52
+ # Every time a fork occurs (a new child process is created), we need to start
53
+ # a DRb service in a background thread within the child process. This service
54
+ # will manage the connection and handle resource cleanup.
55
+ def handle_fork
56
+ @has_forked = true
57
+ DRb.start_service
58
+ # we need to ensure that there are not more jobs in the queue, but
59
+ # we reuse the same object
60
+ @worker.restart
61
+ schedule_tasks
62
+ end
63
+
64
+ private
65
+
66
+ def schedule_tasks
67
+ @worker.every(@heartbeat_interval, run_now: false) { send_collector_events }
68
+
69
+ # Runtime_settings fetch must happens only in the child processes, otherwise, due to
70
+ # we are updating the global runtime_settings, we could have an infinite recursion.
71
+ if @has_forked
72
+ @worker.every(@polling_interval) do
73
+ Aikido::Zen.runtime_settings = @front_object.updated_settings
74
+ @config.logger.debug "Updated runtime settings after polling from child process #{Process.pid}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # dRB Front object that will work as a bridge communication between child & parent
4
+ # processes.
5
+ # Every method is called from the child but it runs in the parent process.
6
+ module Aikido::Zen::DetachedAgent
7
+ class FrontObject
8
+ def initialize(
9
+ config: Aikido::Zen.config,
10
+ runtime_settings: Aikido::Zen.runtime_settings,
11
+ collector: Aikido::Zen.collector,
12
+ rate_limiter: Aikido::Zen::RateLimiter.new
13
+ )
14
+ @config = config
15
+ @runtime_settings = runtime_settings
16
+ @collector = collector
17
+ @rate_limiter = rate_limiter
18
+ end
19
+
20
+ RequestKind = Struct.new(:route, :schema, :ip, :actor)
21
+
22
+ def send_collector_events(events_data)
23
+ events_data.each do |event_data|
24
+ event = Aikido::Zen::Collector::Event.from_json(event_data)
25
+ @collector.add_event(event)
26
+ end
27
+ end
28
+
29
+ # Method called by child processes to get an up-to-date version of the
30
+ # runtime_settings
31
+ def updated_settings
32
+ @runtime_settings
33
+ end
34
+
35
+ def calculate_rate_limits(route_data, ip, actor_data)
36
+ actor = Aikido::Zen::Actor.from_json(actor_data) if actor_data
37
+ route = Aikido::Zen::Route.from_json(route_data)
38
+ @rate_limiter.calculate_rate_limits(RequestKind.new(route, nil, ip, actor))
39
+ end
40
+ end
41
+ end