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
@@ -0,0 +1,279 @@
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
+ # @api private
12
+ # @return [Boolean] whether Aikido should protect.
13
+ def protect?
14
+ !api_token.nil? || blocking_mode? || debugging?
15
+ end
16
+
17
+ # @return [Boolean] whether Aikido should be turned completely off (no
18
+ # intercepting calls to protect the app, no agent process running, no
19
+ # middleware installed). Defaults to false (so, enabled). Can be set
20
+ # via the AIKIDO_DISABLED environment variable.
21
+ attr_accessor :disabled
22
+ alias_method :disabled?, :disabled
23
+
24
+ # @return [Boolean] whether Aikido should only report infractions or block
25
+ # the request by raising an Exception. Defaults to whether AIKIDO_BLOCK
26
+ # is set to a non-empty value in your environment, or +false+ otherwise.
27
+ attr_accessor :blocking_mode
28
+ alias_method :blocking_mode?, :blocking_mode
29
+
30
+ # @return [URI] The HTTP host for the Aikido API. Defaults to
31
+ # +https://guard.aikido.dev+.
32
+ attr_reader :api_endpoint
33
+
34
+ # @return [URI] The HTTP host for the Aikido Runtime API. Defaults to
35
+ # +https://runtime.aikido.dev+.
36
+ attr_reader :realtime_endpoint
37
+
38
+ # @return [Hash] HTTP timeouts for communicating with the API.
39
+ attr_reader :api_timeouts
40
+
41
+ # @return [String] the token obtained when configuring the Firewall in the
42
+ # Aikido interface.
43
+ attr_accessor :api_token
44
+
45
+ # @return [Integer] the interval in seconds to poll the runtime API for
46
+ # settings changes. Defaults to evey 60 seconds.
47
+ attr_accessor :polling_interval
48
+
49
+ # @return [Integer] the amount in seconds to wait before sending an initial
50
+ # heartbeat event when the server reports no stats have been sent yet.
51
+ attr_accessor :initial_heartbeat_delay
52
+
53
+ # @return [#call] Callable that can be passed an Object and returns a String
54
+ # of JSON. Defaults to the standard library's JSON.dump method.
55
+ attr_accessor :json_encoder
56
+
57
+ # @return [#call] Callable that can be passed a JSON string and parses it
58
+ # into an Object. Defaults to the standard library's JSON.parse method.
59
+ attr_accessor :json_decoder
60
+
61
+ # @return [Logger]
62
+ attr_reader :logger
63
+
64
+ # @return [string] Path of the socket where the detached agent will listen.
65
+ # By default, is stored under the root application path with file name
66
+ # `aikido-detached-agent.sock`
67
+ attr_reader :detached_agent_socket_path
68
+
69
+ # @return [Boolean] is the agent in debugging mode?
70
+ attr_accessor :debugging
71
+ alias_method :debugging?, :debugging
72
+
73
+ # @return [Integer] maximum number of timing measurements to keep in memory
74
+ # before compressing them.
75
+ attr_accessor :max_performance_samples
76
+
77
+ # @return [Integer] maximum number of compressed performance samples to keep
78
+ # in memory. If we take more than this before reporting them to Aikido, we
79
+ # will discard the oldest samples.
80
+ attr_accessor :max_compressed_stats
81
+
82
+ # @return [Integer] maximum number of connections to outbound hosts to keep
83
+ # in memory in order to report them in the next heartbeat event. If new
84
+ # connections are added to the set before reporting them to Aikido, we
85
+ # will discard the oldest data point.
86
+ attr_accessor :max_outbound_connections
87
+
88
+ # @return [Integer] maximum number of users tracked via Zen.track_user to
89
+ # share with the Aikido servers on the next heartbeat event. If more
90
+ # unique users (by their ID) are tracked than this number, we will discard
91
+ # the oldest seen users.
92
+ attr_accessor :max_users_tracked
93
+
94
+ # @return [Proc{(Aikido::Zen::Request, Symbol) => Array(Integer, Hash, #each)}]
95
+ # Rack handler used to respond to requests from IPs, users or others blocked in the Aikido
96
+ # dashboard.
97
+ attr_accessor :blocked_responder
98
+
99
+ # @return [Proc{Aikido::Zen::Request => Array(Integer, Hash, #each)}]
100
+ # Rack handler used to respond to requests that have been rate limited.
101
+ attr_accessor :rate_limited_responder
102
+
103
+ # @return [Proc{Aikido::Zen::Request => String}] a proc that reads
104
+ # information off the current request and returns a String to
105
+ # differentiate different clients. By default this uses the request IP.
106
+ attr_accessor :rate_limiting_discriminator
107
+
108
+ # @return [Boolean] whether Aikido Zen should collect api schemas.
109
+ # Defaults to true. Can be set through AIKIDO_FEATURE_COLLECT_API_SCHEMA
110
+ # environment variable.
111
+ attr_accessor :collect_api_schema
112
+ alias_method :collect_api_schema?, :collect_api_schema
113
+
114
+ # @return [Integer] max number of requests we sample per endpoint when
115
+ # computing the schema.
116
+ attr_accessor :api_schema_max_samples
117
+
118
+ # @api private
119
+ # @return [Integer] max number of levels deep we want to read a nested
120
+ # strcture for performance reasons.
121
+ attr_accessor :api_schema_collection_max_depth
122
+
123
+ # @api private
124
+ # @return [Integer] max number of properties that we want to inspect per
125
+ # level of the structure for performance reasons.
126
+ attr_accessor :api_schema_collection_max_properties
127
+
128
+ # @api private
129
+ # @return [Proc<Hash => Aikido::Zen::Context>] callable that takes a
130
+ # Rack-compatible env Hash and returns a Context object with an HTTP
131
+ # request. This is meant to be overridden by each framework adapter.
132
+ attr_accessor :request_builder
133
+
134
+ # @api private
135
+ # @return [Integer] number of seconds to perform client-side rate limiting
136
+ # of events sent to the server.
137
+ attr_accessor :client_rate_limit_period
138
+
139
+ # @api private
140
+ # @return [Integer] max number of events sent during a sliding
141
+ # {client_rate_limit_period} window.
142
+ attr_accessor :client_rate_limit_max_events
143
+
144
+ # @api private
145
+ # @return [Integer] number of seconds to wait before sending an event after
146
+ # the server returns a 429 response.
147
+ attr_accessor :server_rate_limit_deadline
148
+
149
+ # @return [Array<String>] when checking for stored SSRF attacks, we want to
150
+ # allow known hosts that should be able to resolve to the IMDS service.
151
+ attr_accessor :imds_allowed_hosts
152
+
153
+ def initialize
154
+ self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
155
+ self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
156
+ self.api_timeouts = 10
157
+ self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT", DEFAULT_AIKIDO_ENDPOINT)
158
+ self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
159
+ self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
160
+ self.polling_interval = 60
161
+ self.initial_heartbeat_delay = 60
162
+ self.json_encoder = DEFAULT_JSON_ENCODER
163
+ self.json_decoder = DEFAULT_JSON_DECODER
164
+ self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
165
+ self.logger = Logger.new($stdout, progname: "aikido", level: debugging ? Logger::DEBUG : Logger::INFO)
166
+ self.max_performance_samples = 5000
167
+ self.detached_agent_socket_path = ENV.fetch("AIKIDO_DETACHED_AGENT_SOCKET_PATH", DEFAULT_DETACHED_AGENT_SOCKET_PATH)
168
+ self.max_compressed_stats = 100
169
+ self.max_outbound_connections = 200
170
+ self.max_users_tracked = 1000
171
+ self.request_builder = Aikido::Zen::Context::RACK_REQUEST_BUILDER
172
+ self.blocked_responder = DEFAULT_BLOCKED_RESPONDER
173
+ self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
174
+ self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
175
+ self.server_rate_limit_deadline = 1800 # 30 min
176
+ self.client_rate_limit_period = 3600 # 1 hour
177
+ self.client_rate_limit_max_events = 100
178
+ self.collect_api_schema = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", true))
179
+ self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
180
+ self.api_schema_collection_max_depth = 20
181
+ self.api_schema_collection_max_properties = 20
182
+ self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
183
+ end
184
+
185
+ # Set the base URL for API requests.
186
+ #
187
+ # @param url [String, URI]
188
+ def api_endpoint=(url)
189
+ @api_endpoint = URI(url)
190
+ end
191
+
192
+ # Set the base URL for runtime API requests.
193
+ #
194
+ # @param url [String, URI]
195
+ def realtime_endpoint=(url)
196
+ @realtime_endpoint = URI(url)
197
+ end
198
+
199
+ # Set the logger and configure its severity level according to agent's debug mode
200
+ # @param logger [::Logger]
201
+ def logger=(logger)
202
+ @logger = logger
203
+ @logger.level = Logger::DEBUG if debugging
204
+ end
205
+
206
+ # @overload def api_timeouts=(timeouts)
207
+ # Configure granular connection timeouts for the Aikido Zen API. You
208
+ # can set any of these per call.
209
+ # @param timeouts [Hash]
210
+ # @option timeouts [Integer] :open_timeout Duration in seconds.
211
+ # @option timeouts [Integer] :read_timeout Duration in seconds.
212
+ # @option timeouts [Integer] :write_timeout Duration in seconds.
213
+ #
214
+ # @overload def api_timeouts=(duration)
215
+ # Configure the connection timeouts for the Aikido Zen API.
216
+ # @param duration [Integer] Duration in seconds to set for all three
217
+ # timeouts (open, read, and write).
218
+ def api_timeouts=(value)
219
+ value = {open_timeout: value, read_timeout: value, write_timeout: value} if value.respond_to?(:to_int)
220
+
221
+ @api_timeouts ||= {}
222
+ @api_timeouts.update(value)
223
+ end
224
+
225
+ def detached_agent_socket_path=(path)
226
+ @detached_agent_socket_path = path
227
+ @detached_agent_socket_path = "drbunix:" + @detached_agent_socket_path unless @detached_agent_socket_path.start_with?("drbunix:")
228
+ end
229
+
230
+ private
231
+
232
+ def read_boolean_from_env(value)
233
+ return value unless value.respond_to?(:to_str)
234
+
235
+ case value.to_str.strip
236
+ when "false", "", "0", "f"
237
+ false
238
+ else
239
+ true
240
+ end
241
+ end
242
+
243
+ # @!visibility private
244
+ DEFAULT_AIKIDO_ENDPOINT = "https://guard.aikido.dev"
245
+
246
+ # @!visibility private
247
+ DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
248
+
249
+ # @!visibility private
250
+ DEFAULT_JSON_ENCODER = JSON.method(:dump)
251
+
252
+ # @!visibility private
253
+ DEFAULT_JSON_DECODER = JSON.method(:parse)
254
+
255
+ # @!visibility private
256
+ DEFAULT_DETACHED_AGENT_SOCKET_PATH = "aikido-detached-agent.sock"
257
+
258
+ # @!visibility private
259
+ DEFAULT_BLOCKED_RESPONDER = ->(request, blocking_type) do
260
+ message = case blocking_type
261
+ when :ip
262
+ format("Your IP address is not allowed to access this resource. (Your IP: %s)", request.ip)
263
+ else
264
+ "You are blocked by Zen."
265
+ end
266
+ [403, {"Content-Type" => "text/plain"}, [message]]
267
+ end
268
+
269
+ # @!visibility private
270
+ DEFAULT_RATE_LIMITED_RESPONDER = ->(request) do
271
+ [429, {"Content-Type" => "text/plain"}, ["Too many requests."]]
272
+ end
273
+
274
+ # @!visibility private
275
+ DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
276
+ request.actor ? "actor:#{request.actor.id}" : request.ip
277
+ }
278
+ end
279
+ end
@@ -0,0 +1,24 @@
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
+ delegate = Rack::Request.new(env)
10
+ router = Aikido::Zen::Request::HeuristicRouter.new
11
+ request = Aikido::Zen::Request.new(delegate, framework: "rack", router: router)
12
+
13
+ Context.new(request) do |req|
14
+ {
15
+ query: req.GET,
16
+ body: req.POST,
17
+ route: {},
18
+ header: req.normalized_headers,
19
+ cookie: req.cookies,
20
+ subdomain: []
21
+ }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
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
+ delegate = ActionDispatch::Request.new(env)
16
+ request = Aikido::Zen::Request.new(
17
+ delegate, framework: "rails", router: Rails.router
18
+ )
19
+
20
+ decrypt_cookies = ->(req) do
21
+ return req.cookies unless req.respond_to?(:cookie_jar)
22
+
23
+ req.cookie_jar.map { |key, value|
24
+ plain_text = req.cookie_jar.encrypted[key].presence ||
25
+ req.cookie_jar.signed[key].presence ||
26
+ value
27
+ [key, plain_text]
28
+ }.to_h
29
+ end
30
+
31
+ Context.new(request) do |req|
32
+ {
33
+ query: req.query_parameters,
34
+ body: req.request_parameters,
35
+ route: req.path_parameters,
36
+ header: req.normalized_headers,
37
+ cookie: decrypt_cookies.call(req),
38
+ subdomain: req.subdomains
39
+ }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,112 @@
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[request.route].protected? ||
85
+ @settings.skip_protection_for_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
+ def extract_payloads_from(data, source_type, prefix = nil)
96
+ if data.respond_to?(:to_hash)
97
+ data.to_hash.flat_map { |name, val|
98
+ extract_payloads_from(val, source_type, [prefix, name].compact.join("."))
99
+ }
100
+ elsif data.respond_to?(:to_ary)
101
+ data.to_ary.flat_map.with_index { |val, idx|
102
+ extract_payloads_from(val, source_type, [prefix, idx].compact.join("."))
103
+ }
104
+ else
105
+ Payload.new(data, source_type, prefix.to_s)
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ require_relative "context/rack_request"
112
+ require_relative "context/rails_request"
@@ -0,0 +1,78 @@
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
+ # @detached_agent_front 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
+ heartbeat_interval: 10,
25
+ polling_interval: 10,
26
+ config: Aikido::Zen.config,
27
+ collector: Aikido::Zen.collector,
28
+ worker: Aikido::Zen::Worker.new(config: config)
29
+ )
30
+ @config = config
31
+ @heartbeat_interval = heartbeat_interval
32
+ @polling_interval = polling_interval
33
+ @worker = worker
34
+ @collector = collector
35
+ @detached_agent_front = DRbObject.new_with_uri(config.detached_agent_socket_path)
36
+ @has_forked = false
37
+ schedule_tasks
38
+ end
39
+
40
+ def send_heartbeat(at: Time.now.utc)
41
+ return unless @collector.stats.any?
42
+
43
+ heartbeat = @collector.flush(at: at)
44
+ @detached_agent_front.send_heartbeat_to_parent_process(heartbeat.as_json)
45
+ end
46
+
47
+ private def schedule_tasks
48
+ # For heartbeats is correct to send them from parent or child process. Otherwise, we'll lose
49
+ # stats made by the parent process.
50
+ @worker.every(@heartbeat_interval, run_now: false) { send_heartbeat }
51
+
52
+ # Runtime_settings fetch must happens only in the child processes, otherwise, due to
53
+ # we are updating the global runtime_settings, we could have an infinite recursion.
54
+ if @has_forked
55
+ @worker.every(@polling_interval) do
56
+ Aikido::Zen.runtime_settings = @detached_agent_front.updated_settings
57
+ @config.logger.debug "Updated runtime settings after polling from child process #{Process.pid}"
58
+ end
59
+ end
60
+ end
61
+
62
+ def calculate_rate_limits(request)
63
+ @detached_agent_front.calculate_rate_limits(request.route, request.ip, request.actor.to_json)
64
+ end
65
+
66
+ # Every time a fork occurs (a new child process is created), we need to start
67
+ # a DRb service in a background thread within the child process. This service
68
+ # will manage the connection and handle resource cleanup.
69
+ def handle_fork
70
+ @has_forked = true
71
+ DRb.start_service
72
+ # we need to ensure that there are not more jobs in the queue, but
73
+ # we reuse the same object
74
+ @worker.restart
75
+ schedule_tasks
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,37 @@
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
+ collector: Aikido::Zen.collector,
11
+ runtime_settings: Aikido::Zen.runtime_settings,
12
+ rate_limiter: Aikido::Zen::RateLimiter.new
13
+ )
14
+ @config = config
15
+ @collector = collector
16
+ @rate_limiter = rate_limiter
17
+ @runtime_settings = runtime_settings
18
+ end
19
+
20
+ RequestKind = Struct.new(:route, :schema, :ip, :actor)
21
+
22
+ def send_heartbeat_to_parent_process(heartbeat)
23
+ @collector.push_heartbeat(heartbeat)
24
+ end
25
+
26
+ # Method called by child processes to get an up-to-date version of the
27
+ # runtime_settings
28
+ def updated_settings
29
+ @runtime_settings
30
+ end
31
+
32
+ def calculate_rate_limits(route, ip, actor_hash)
33
+ actor = Aikido::Zen::Actor(actor_hash) if actor_hash
34
+ @rate_limiter.calculate_rate_limits(RequestKind.new(route, nil, ip, actor))
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen::DetachedAgent
4
+ class Server
5
+ def initialize(config: Aikido::Zen.config)
6
+ @detached_agent_front = FrontObject.new
7
+ @drb_server = DRb.start_service(config.detached_agent_socket_path, @detached_agent_front)
8
+
9
+ # We don't want to see drb logs unless in debug mode
10
+ @drb_server.verbose = config.logger.debug?
11
+ end
12
+
13
+ def alive?
14
+ @drb_server.alive?
15
+ end
16
+
17
+ def stop!
18
+ @drb_server.stop_service
19
+ DRb.stop_service
20
+ end
21
+
22
+ class << self
23
+ def start!
24
+ Aikido::Zen.config.logger.debug("Starting DRb Server...")
25
+ max_attempts = 10
26
+ @server = new
27
+
28
+ attempts = 0
29
+ until @server.alive?
30
+ Aikido::Zen.config.logger.info("DRb Server still not alive. #{max_attempts - attempts} attempts remaining")
31
+ sleep 0.1
32
+ attempts += 1
33
+ raise Aikido::Zen::DetachedAgentError.new("Impossible to start the dRB server (socket=#{Aikido::Zen.config.detached_agent_socket_path})") \
34
+ if attempts == max_attempts
35
+ end
36
+
37
+ @server
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "detached_agent/agent"
2
+ require_relative "detached_agent/server"