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
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Handles collecting all the runtime statistics to report back to the Aikido
5
+ # servers.
6
+ class Collector
7
+ def initialize(config: Aikido::Zen.config)
8
+ @config = config
9
+
10
+ @stats = Concurrent::AtomicReference.new(Stats.new(@config))
11
+ @users = Concurrent::AtomicReference.new(Users.new(@config))
12
+ @hosts = Concurrent::AtomicReference.new(Hosts.new(@config))
13
+ @routes = Concurrent::AtomicReference.new(Routes.new(@config))
14
+ @heartbeats = Queue.new
15
+ @middleware_installed = Concurrent::AtomicBoolean.new
16
+ end
17
+
18
+ # Flush all the stats into a Heartbeat event that can be reported back to
19
+ # the Aikido servers.
20
+ #
21
+ # @param at [Time] the time at which stats collection stopped and the start
22
+ # of the new stats collection period. Defaults to now.
23
+ # @return [Aikido::Zen::Events::Heartbeat]
24
+ def flush(at: Time.now.utc)
25
+ stats = @stats.get_and_set(Stats.new(@config))
26
+ users = @users.get_and_set(Users.new(@config))
27
+ hosts = @hosts.get_and_set(Hosts.new(@config))
28
+ routes = @routes.get_and_set(Routes.new(@config))
29
+
30
+ start(at: at)
31
+ stats = stats.flush(at: at)
32
+
33
+ Events::Heartbeat.new(
34
+ stats: stats, users: users, hosts: hosts, routes: routes, middleware_installed: middleware_installed?
35
+ )
36
+ end
37
+
38
+ # Put heartbeats coming from child processes into the internal queue.
39
+ def push_heartbeat(heartbeat)
40
+ @heartbeats << heartbeat
41
+ end
42
+
43
+ # Drains into an array all the queued heartbeats
44
+ def flush_heartbeats
45
+ Array.new(@heartbeats.size) { @heartbeats.pop }
46
+ end
47
+
48
+ # Sets the start time for this collection period.
49
+ #
50
+ # @param at [Time] defaults to now.
51
+ # @return [void]
52
+ def start(at: Time.now.utc)
53
+ synchronize(@stats) { |stats| stats.start(at) }
54
+ end
55
+
56
+ # Track stats about the requests
57
+ #
58
+ # @param request [Aikido::Zen::Request]
59
+ # @return [void]
60
+ def track_request(*)
61
+ synchronize(@stats) { |stats| stats.add_request }
62
+ end
63
+
64
+ # Record the visited endpoint, and if enabled, the API schema for this endpoint.
65
+ # @param request [Aikido::Zen::Request]
66
+ def track_route(request)
67
+ synchronize(@routes) { |routes| routes.add(request) if request.route }
68
+ end
69
+
70
+ # Track stats about a scan performed by one of our sinks.
71
+ #
72
+ # @param scan [Aikido::Zen::Scan]
73
+ # @return [void]
74
+ def track_scan(scan)
75
+ synchronize(@stats) { |stats| stats.add_scan(scan) }
76
+ end
77
+
78
+ # Track stats about an attack detected by our scanners.
79
+ #
80
+ # @param attack [Aikido::Zen::Attack]
81
+ # @return [void]
82
+ def track_attack(attack)
83
+ synchronize(@stats) do |stats|
84
+ stats.add_attack(attack, being_blocked: attack.blocked?)
85
+ end
86
+ end
87
+
88
+ # Track an HTTP connections to an external host.
89
+ #
90
+ # @param connection [Aikido::Zen::OutboundConnection]
91
+ # @return [void]
92
+ def track_outbound(connection)
93
+ synchronize(@hosts) { |hosts| hosts.add(connection) }
94
+ end
95
+
96
+ # Track the user reported by the developer to be behind this request.
97
+ #
98
+ # @param actor [Aikido::Zen::Actor]
99
+ # @return [void]
100
+ def track_user(actor)
101
+ synchronize(@users) { |users| users.add(actor) }
102
+ end
103
+
104
+ def middleware_installed!
105
+ @middleware_installed.make_true
106
+ end
107
+
108
+ # @api private
109
+ def routes
110
+ @routes.get
111
+ end
112
+
113
+ # @api private
114
+ def users
115
+ @users.get
116
+ end
117
+
118
+ # @api private
119
+ def hosts
120
+ @hosts.get
121
+ end
122
+
123
+ # @api private
124
+ def stats
125
+ @stats.get
126
+ end
127
+
128
+ # @api private
129
+ def middleware_installed?
130
+ @middleware_installed.true?
131
+ end
132
+
133
+ # Atomically modify an object's state within a block, ensuring it's safe
134
+ # from other threads.
135
+ private def synchronize(object)
136
+ object.update { |obj| obj.tap { yield obj } }
137
+ end
138
+ end
139
+ end
140
+
141
+ require_relative "collector/stats"
142
+ require_relative "collector/users"
143
+ require_relative "collector/hosts"
144
+ require_relative "collector/routes"
@@ -0,0 +1,282 @@
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_accessor :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
+ # @return [String] environment specific HTTP header providing the client IP.
154
+ attr_accessor :client_ip_header
155
+
156
+ def initialize
157
+ self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
158
+ self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
159
+ self.api_timeouts = 10
160
+ self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT", DEFAULT_AIKIDO_ENDPOINT)
161
+ self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
162
+ self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
163
+ self.polling_interval = 60
164
+ self.initial_heartbeat_delay = 60
165
+ self.json_encoder = DEFAULT_JSON_ENCODER
166
+ self.json_decoder = DEFAULT_JSON_DECODER
167
+ self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
168
+ self.logger = Logger.new($stdout, progname: "aikido", level: debugging ? Logger::DEBUG : Logger::INFO)
169
+ self.detached_agent_socket_path = ENV.fetch("AIKIDO_DETACHED_AGENT_SOCKET_PATH", DEFAULT_DETACHED_AGENT_SOCKET_PATH)
170
+ self.client_ip_header = ENV.fetch("AIKIDO_CLIENT_IP_HEADER", nil)
171
+ self.max_performance_samples = 5000
172
+ self.max_compressed_stats = 100
173
+ self.max_outbound_connections = 200
174
+ self.max_users_tracked = 1000
175
+ self.request_builder = Aikido::Zen::Context::RACK_REQUEST_BUILDER
176
+ self.blocked_responder = DEFAULT_BLOCKED_RESPONDER
177
+ self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
178
+ self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
179
+ self.server_rate_limit_deadline = 1800 # 30 min
180
+ self.client_rate_limit_period = 3600 # 1 hour
181
+ self.client_rate_limit_max_events = 100
182
+ self.collect_api_schema = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", true))
183
+ self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
184
+ self.api_schema_collection_max_depth = 20
185
+ self.api_schema_collection_max_properties = 20
186
+ self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
187
+ end
188
+
189
+ # Set the base URL for API requests.
190
+ #
191
+ # @param url [String, URI]
192
+ def api_endpoint=(url)
193
+ @api_endpoint = URI(url)
194
+ end
195
+
196
+ # Set the base URL for runtime API requests.
197
+ #
198
+ # @param url [String, URI]
199
+ def realtime_endpoint=(url)
200
+ @realtime_endpoint = URI(url)
201
+ end
202
+
203
+ # Set the logger and configure its severity level according to agent's debug mode
204
+ # @param logger [::Logger]
205
+ def logger=(logger)
206
+ @logger = logger
207
+ @logger.level = Logger::DEBUG if debugging
208
+ end
209
+
210
+ # @overload def api_timeouts=(timeouts)
211
+ # Configure granular connection timeouts for the Aikido Zen API. You
212
+ # can set any of these per call.
213
+ # @param timeouts [Hash]
214
+ # @option timeouts [Integer] :open_timeout Duration in seconds.
215
+ # @option timeouts [Integer] :read_timeout Duration in seconds.
216
+ # @option timeouts [Integer] :write_timeout Duration in seconds.
217
+ #
218
+ # @overload def api_timeouts=(duration)
219
+ # Configure the connection timeouts for the Aikido Zen API.
220
+ # @param duration [Integer] Duration in seconds to set for all three
221
+ # timeouts (open, read, and write).
222
+ def api_timeouts=(value)
223
+ value = {open_timeout: value, read_timeout: value, write_timeout: value} if value.respond_to?(:to_int)
224
+
225
+ @api_timeouts ||= {}
226
+ @api_timeouts.update(value)
227
+ end
228
+
229
+ def detached_agent_socket_uri
230
+ "drbunix:" + @detached_agent_socket_path
231
+ end
232
+
233
+ private
234
+
235
+ def read_boolean_from_env(value)
236
+ return value unless value.respond_to?(:to_str)
237
+
238
+ case value.to_str.strip
239
+ when "false", "", "0", "f"
240
+ false
241
+ else
242
+ true
243
+ end
244
+ end
245
+
246
+ # @!visibility private
247
+ DEFAULT_AIKIDO_ENDPOINT = "https://guard.aikido.dev"
248
+
249
+ # @!visibility private
250
+ DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
251
+
252
+ # @!visibility private
253
+ DEFAULT_JSON_ENCODER = JSON.method(:dump)
254
+
255
+ # @!visibility private
256
+ DEFAULT_JSON_DECODER = JSON.method(:parse)
257
+
258
+ # @!visibility private
259
+ DEFAULT_DETACHED_AGENT_SOCKET_PATH = "aikido-detached-agent.sock"
260
+
261
+ # @!visibility private
262
+ DEFAULT_BLOCKED_RESPONDER = ->(request, blocking_type) do
263
+ message = case blocking_type
264
+ when :ip
265
+ format("Your IP address is not allowed to access this resource. (Your IP: %s)", request.ip)
266
+ else
267
+ "You are blocked by Zen."
268
+ end
269
+ [403, {"Content-Type" => "text/plain"}, [message]]
270
+ end
271
+
272
+ # @!visibility private
273
+ DEFAULT_RATE_LIMITED_RESPONDER = ->(request) do
274
+ [429, {"Content-Type" => "text/plain"}, ["Too many requests."]]
275
+ end
276
+
277
+ # @!visibility private
278
+ DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
279
+ request.actor ? "actor:#{request.actor.id}" : request.ip
280
+ }
281
+ end
282
+ 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,44 @@
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
+ # Duplicate the Rack environment to prevent unexpected modifications from
16
+ # breaking Rails routing.
17
+ delegate = ActionDispatch::Request.new(env.dup)
18
+ request = Aikido::Zen::Request.new(
19
+ delegate, framework: "rails", router: Rails.router
20
+ )
21
+
22
+ decrypt_cookies = ->(req) do
23
+ return req.cookies unless req.respond_to?(:cookie_jar)
24
+
25
+ req.cookie_jar.map { |key, value|
26
+ plain_text = req.cookie_jar.encrypted[key].presence ||
27
+ req.cookie_jar.signed[key].presence ||
28
+ value
29
+ [key, plain_text]
30
+ }.to_h
31
+ end
32
+
33
+ Context.new(request) do |req|
34
+ {
35
+ query: req.query_parameters,
36
+ body: req.request_parameters,
37
+ route: req.path_parameters,
38
+ header: req.normalized_headers,
39
+ cookie: decrypt_cookies.call(req),
40
+ subdomain: req.subdomains
41
+ }
42
+ end
43
+ end
44
+ 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_uri)
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