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.
- checksums.yaml +7 -0
- data/.aikido +6 -0
- data/.ruby-version +1 -0
- data/.simplecov +32 -0
- data/.standard.yml +3 -0
- data/LICENSE +674 -0
- data/README.md +148 -0
- data/Rakefile +67 -0
- data/benchmarks/README.md +22 -0
- data/benchmarks/rails7.1_benchmark.js +1 -0
- data/benchmarks/rails7.1_sql_injection.js +102 -0
- data/docs/banner.svg +202 -0
- data/docs/config.md +133 -0
- data/docs/proxy.md +10 -0
- data/docs/rails.md +112 -0
- data/docs/troubleshooting.md +62 -0
- data/lib/aikido/zen/actor.rb +146 -0
- data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
- data/lib/aikido/zen/agent.rb +181 -0
- data/lib/aikido/zen/api_client.rb +145 -0
- data/lib/aikido/zen/attack.rb +217 -0
- data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
- data/lib/aikido/zen/attack_wave.rb +88 -0
- data/lib/aikido/zen/background_worker.rb +52 -0
- data/lib/aikido/zen/cache.rb +91 -0
- data/lib/aikido/zen/capped_collections.rb +86 -0
- data/lib/aikido/zen/collector/event.rb +238 -0
- data/lib/aikido/zen/collector/hosts.rb +30 -0
- data/lib/aikido/zen/collector/routes.rb +71 -0
- data/lib/aikido/zen/collector/sink_stats.rb +95 -0
- data/lib/aikido/zen/collector/stats.rb +122 -0
- data/lib/aikido/zen/collector/users.rb +32 -0
- data/lib/aikido/zen/collector.rb +223 -0
- data/lib/aikido/zen/config.rb +312 -0
- data/lib/aikido/zen/context/rack_request.rb +27 -0
- data/lib/aikido/zen/context/rails_request.rb +47 -0
- data/lib/aikido/zen/context.rb +145 -0
- data/lib/aikido/zen/detached_agent/agent.rb +79 -0
- data/lib/aikido/zen/detached_agent/front_object.rb +41 -0
- data/lib/aikido/zen/detached_agent/server.rb +78 -0
- data/lib/aikido/zen/detached_agent.rb +2 -0
- data/lib/aikido/zen/errors.rb +107 -0
- data/lib/aikido/zen/event.rb +116 -0
- data/lib/aikido/zen/helpers.rb +24 -0
- data/lib/aikido/zen/internals.rb +123 -0
- data/lib/aikido/zen/libzen-v0.1.48-aarch64-linux.so +0 -0
- data/lib/aikido/zen/middleware/allowed_address_checker.rb +26 -0
- data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
- data/lib/aikido/zen/middleware/context_setter.rb +26 -0
- data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
- data/lib/aikido/zen/middleware/middleware.rb +11 -0
- data/lib/aikido/zen/middleware/rack_throttler.rb +50 -0
- data/lib/aikido/zen/middleware/request_tracker.rb +197 -0
- data/lib/aikido/zen/outbound_connection.rb +62 -0
- data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
- data/lib/aikido/zen/package.rb +22 -0
- data/lib/aikido/zen/payload.rb +50 -0
- data/lib/aikido/zen/rails_engine.rb +53 -0
- data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
- data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
- data/lib/aikido/zen/rate_limiter/result.rb +31 -0
- data/lib/aikido/zen/rate_limiter.rb +50 -0
- data/lib/aikido/zen/request/heuristic_router.rb +115 -0
- data/lib/aikido/zen/request/rails_router.rb +92 -0
- data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
- data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
- data/lib/aikido/zen/request/schema/builder.rb +121 -0
- data/lib/aikido/zen/request/schema/definition.rb +107 -0
- data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
- data/lib/aikido/zen/request/schema.rb +87 -0
- data/lib/aikido/zen/request.rb +88 -0
- data/lib/aikido/zen/route.rb +96 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +78 -0
- data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
- data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
- data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
- data/lib/aikido/zen/runtime_settings.rb +66 -0
- data/lib/aikido/zen/scan.rb +75 -0
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +68 -0
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +64 -0
- data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +65 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +94 -0
- data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +266 -0
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +55 -0
- data/lib/aikido/zen/scanners.rb +7 -0
- data/lib/aikido/zen/sink.rb +118 -0
- data/lib/aikido/zen/sinks/action_controller.rb +85 -0
- data/lib/aikido/zen/sinks/async_http.rb +80 -0
- data/lib/aikido/zen/sinks/curb.rb +113 -0
- data/lib/aikido/zen/sinks/em_http.rb +83 -0
- data/lib/aikido/zen/sinks/excon.rb +118 -0
- data/lib/aikido/zen/sinks/file.rb +153 -0
- data/lib/aikido/zen/sinks/http.rb +93 -0
- data/lib/aikido/zen/sinks/httpclient.rb +95 -0
- data/lib/aikido/zen/sinks/httpx.rb +78 -0
- data/lib/aikido/zen/sinks/kernel.rb +33 -0
- data/lib/aikido/zen/sinks/mysql2.rb +31 -0
- data/lib/aikido/zen/sinks/net_http.rb +101 -0
- data/lib/aikido/zen/sinks/patron.rb +103 -0
- data/lib/aikido/zen/sinks/pg.rb +72 -0
- data/lib/aikido/zen/sinks/resolv.rb +62 -0
- data/lib/aikido/zen/sinks/socket.rb +85 -0
- data/lib/aikido/zen/sinks/sqlite3.rb +46 -0
- data/lib/aikido/zen/sinks/trilogy.rb +31 -0
- data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
- data/lib/aikido/zen/sinks.rb +36 -0
- data/lib/aikido/zen/sinks_dsl.rb +250 -0
- data/lib/aikido/zen/synchronizable.rb +24 -0
- data/lib/aikido/zen/system_info.rb +80 -0
- data/lib/aikido/zen/version.rb +10 -0
- data/lib/aikido/zen/worker.rb +87 -0
- data/lib/aikido/zen.rb +303 -0
- data/lib/aikido-zen.rb +3 -0
- data/placeholder/.gitignore +4 -0
- data/placeholder/README.md +11 -0
- data/placeholder/Rakefile +75 -0
- data/placeholder/lib/placeholder.rb.template +3 -0
- data/placeholder/placeholder.gemspec.template +20 -0
- data/tasklib/bench.rake +94 -0
- data/tasklib/libzen.rake +133 -0
- data/tasklib/wrk.rb +88 -0
- 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
|