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.
- checksums.yaml +7 -0
- data/.aikido +6 -0
- data/.ruby-version +1 -0
- data/.simplecov +26 -0
- data/.standard.yml +3 -0
- data/LICENSE +674 -0
- data/README.md +146 -0
- data/Rakefile +67 -0
- data/benchmarks/README.md +23 -0
- data/benchmarks/rails7.1_sql_injection.js +70 -0
- data/docs/banner.svg +202 -0
- data/docs/config.md +125 -0
- data/docs/proxy.md +10 -0
- data/docs/rails.md +114 -0
- data/lib/aikido/zen/actor.rb +116 -0
- data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
- data/lib/aikido/zen/agent.rb +179 -0
- data/lib/aikido/zen/api_client.rb +145 -0
- data/lib/aikido/zen/attack.rb +207 -0
- data/lib/aikido/zen/background_worker.rb +52 -0
- data/lib/aikido/zen/capped_collections.rb +68 -0
- data/lib/aikido/zen/collector/hosts.rb +15 -0
- data/lib/aikido/zen/collector/routes.rb +66 -0
- data/lib/aikido/zen/collector/sink_stats.rb +95 -0
- data/lib/aikido/zen/collector/stats.rb +111 -0
- data/lib/aikido/zen/collector/users.rb +30 -0
- data/lib/aikido/zen/collector.rb +144 -0
- data/lib/aikido/zen/config.rb +282 -0
- data/lib/aikido/zen/context/rack_request.rb +24 -0
- data/lib/aikido/zen/context/rails_request.rb +44 -0
- data/lib/aikido/zen/context.rb +112 -0
- data/lib/aikido/zen/detached_agent/agent.rb +78 -0
- data/lib/aikido/zen/detached_agent/front_object.rb +37 -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 +71 -0
- data/lib/aikido/zen/internals.rb +103 -0
- data/lib/aikido/zen/libzen-v0.1.39-aarch64-linux.so +0 -0
- data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
- data/lib/aikido/zen/middleware/middleware.rb +11 -0
- data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
- data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
- data/lib/aikido/zen/middleware/set_context.rb +26 -0
- data/lib/aikido/zen/outbound_connection.rb +45 -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 +56 -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 +77 -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 +122 -0
- data/lib/aikido/zen/route.rb +39 -0
- data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -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 +65 -0
- data/lib/aikido/zen/scan.rb +75 -0
- data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
- data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -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 +265 -0
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -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 +83 -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 +112 -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 +78 -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 +84 -0
- data/lib/aikido/zen/version.rb +10 -0
- data/lib/aikido/zen/worker.rb +87 -0
- data/lib/aikido/zen.rb +246 -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 +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
|