aikido-zen 0.1.0.alpha4-arm64-darwin
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/.ruby-version +1 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +674 -0
- data/README.md +40 -0
- data/Rakefile +63 -0
- data/lib/aikido/zen/actor.rb +116 -0
- data/lib/aikido/zen/agent.rb +187 -0
- data/lib/aikido/zen/api_client.rb +132 -0
- data/lib/aikido/zen/attack.rb +138 -0
- data/lib/aikido/zen/capped_collections.rb +68 -0
- data/lib/aikido/zen/config.rb +229 -0
- data/lib/aikido/zen/context/rack_request.rb +24 -0
- data/lib/aikido/zen/context/rails_request.rb +42 -0
- data/lib/aikido/zen/context.rb +101 -0
- data/lib/aikido/zen/errors.rb +88 -0
- data/lib/aikido/zen/event.rb +66 -0
- data/lib/aikido/zen/internals.rb +64 -0
- data/lib/aikido/zen/libzen-v0.1.26.aarch64.dylib +0 -0
- data/lib/aikido/zen/middleware/check_allowed_addresses.rb +38 -0
- data/lib/aikido/zen/middleware/set_context.rb +26 -0
- data/lib/aikido/zen/middleware/throttler.rb +50 -0
- data/lib/aikido/zen/outbound_connection.rb +45 -0
- data/lib/aikido/zen/outbound_connection_monitor.rb +19 -0
- data/lib/aikido/zen/package.rb +22 -0
- data/lib/aikido/zen/payload.rb +48 -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 +55 -0
- data/lib/aikido/zen/request/heuristic_router.rb +109 -0
- data/lib/aikido/zen/request/rails_router.rb +84 -0
- data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
- data/lib/aikido/zen/request/schema/auth_schemas.rb +40 -0
- data/lib/aikido/zen/request/schema/builder.rb +125 -0
- data/lib/aikido/zen/request/schema/definition.rb +112 -0
- data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
- data/lib/aikido/zen/request/schema.rb +72 -0
- data/lib/aikido/zen/request.rb +97 -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 +70 -0
- data/lib/aikido/zen/scan.rb +75 -0
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +95 -0
- data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +85 -0
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +251 -0
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +43 -0
- data/lib/aikido/zen/scanners.rb +5 -0
- data/lib/aikido/zen/sink.rb +108 -0
- data/lib/aikido/zen/sinks/async_http.rb +63 -0
- data/lib/aikido/zen/sinks/curb.rb +89 -0
- data/lib/aikido/zen/sinks/em_http.rb +71 -0
- data/lib/aikido/zen/sinks/excon.rb +103 -0
- data/lib/aikido/zen/sinks/http.rb +76 -0
- data/lib/aikido/zen/sinks/httpclient.rb +68 -0
- data/lib/aikido/zen/sinks/httpx.rb +61 -0
- data/lib/aikido/zen/sinks/mysql2.rb +21 -0
- data/lib/aikido/zen/sinks/net_http.rb +85 -0
- data/lib/aikido/zen/sinks/patron.rb +88 -0
- data/lib/aikido/zen/sinks/pg.rb +50 -0
- data/lib/aikido/zen/sinks/resolv.rb +41 -0
- data/lib/aikido/zen/sinks/socket.rb +51 -0
- data/lib/aikido/zen/sinks/sqlite3.rb +30 -0
- data/lib/aikido/zen/sinks/trilogy.rb +21 -0
- data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
- data/lib/aikido/zen/sinks.rb +21 -0
- data/lib/aikido/zen/stats/routes.rb +53 -0
- data/lib/aikido/zen/stats/sink_stats.rb +95 -0
- data/lib/aikido/zen/stats/users.rb +26 -0
- data/lib/aikido/zen/stats.rb +171 -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.rb +138 -0
- data/lib/aikido-zen.rb +3 -0
- data/lib/aikido.rb +3 -0
- data/tasklib/libzen.rake +128 -0
- metadata +175 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Aikido::Zen
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
# Provides a FIFO set with a maximum size. Adding an element after the
|
9
|
+
# capacity has been reached kicks the oldest element in the set out,
|
10
|
+
# while maintaining the uniqueness property of a set (relying on #eql?
|
11
|
+
# and #hash).
|
12
|
+
class CappedSet
|
13
|
+
include Enumerable
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
def_delegators :@data, :size, :empty?
|
17
|
+
|
18
|
+
# @return [Integer]
|
19
|
+
attr_reader :capacity
|
20
|
+
|
21
|
+
def initialize(capacity)
|
22
|
+
@data = CappedMap.new(capacity)
|
23
|
+
end
|
24
|
+
|
25
|
+
def <<(element)
|
26
|
+
@data[element] = nil
|
27
|
+
self
|
28
|
+
end
|
29
|
+
alias_method :add, :<<
|
30
|
+
alias_method :push, :<<
|
31
|
+
|
32
|
+
def each(&b)
|
33
|
+
@data.each_key(&b)
|
34
|
+
end
|
35
|
+
|
36
|
+
def as_json
|
37
|
+
map(&:as_json)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @api private
|
42
|
+
#
|
43
|
+
# Provides a FIFO hash-like structure with a maximum size. Adding a new key
|
44
|
+
# after the capacity has been reached kicks the first element pair added out.
|
45
|
+
class CappedMap
|
46
|
+
include Enumerable
|
47
|
+
extend Forwardable
|
48
|
+
|
49
|
+
def_delegators :@data,
|
50
|
+
:[], :fetch, :delete, :key?,
|
51
|
+
:each, :each_key, :each_value,
|
52
|
+
:size, :empty?, :to_hash
|
53
|
+
|
54
|
+
# @return [Integer]
|
55
|
+
attr_reader :capacity
|
56
|
+
|
57
|
+
def initialize(capacity)
|
58
|
+
raise ArgumentError, "cannot set capacity lower than 1: #{capacity}" if capacity < 1
|
59
|
+
@capacity = capacity
|
60
|
+
@data = {}
|
61
|
+
end
|
62
|
+
|
63
|
+
def []=(key, value)
|
64
|
+
@data[key] = value
|
65
|
+
@data.delete(@data.each_key.first) if @data.size > @capacity
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,229 @@
|
|
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 only report infractions or block
|
12
|
+
# the request by raising an Exception. Defaults to whether AIKIDO_BLOCKING
|
13
|
+
# is set to a non-empty value in your environment, or +false+ otherwise.
|
14
|
+
attr_accessor :blocking_mode
|
15
|
+
alias_method :blocking_mode?, :blocking_mode
|
16
|
+
|
17
|
+
# @return [URI] The HTTP host for the Aikido API. Defaults to
|
18
|
+
# +https://guard.aikido.dev+.
|
19
|
+
attr_reader :api_base_url
|
20
|
+
|
21
|
+
# @return [URI] The HTTP host for the Aikido Runtime API. Defaults to
|
22
|
+
# +https://runtime.aikido.dev+.
|
23
|
+
attr_reader :runtime_api_base_url
|
24
|
+
|
25
|
+
# @return [Hash] HTTP timeouts for communicating with the API.
|
26
|
+
attr_reader :api_timeouts
|
27
|
+
|
28
|
+
# @return [String] the token obtained when configuring the Firewall in the
|
29
|
+
# Aikido interface.
|
30
|
+
attr_accessor :api_token
|
31
|
+
|
32
|
+
# @return [Integer] the interval in seconds to poll the runtime API for
|
33
|
+
# settings changes. Defaults to evey 60 seconds.
|
34
|
+
attr_accessor :polling_interval
|
35
|
+
|
36
|
+
# @return [Integer] the amount in seconds to wait before sending an initial
|
37
|
+
# heartbeat event when the server reports no stats have been sent yet.
|
38
|
+
attr_accessor :initial_heartbeat_delay
|
39
|
+
|
40
|
+
# @return [#call] Callable that can be passed an Object and returns a String
|
41
|
+
# of JSON. Defaults to the standard library's JSON.dump method.
|
42
|
+
attr_accessor :json_encoder
|
43
|
+
|
44
|
+
# @return [#call] Callable that can be passed a JSON string and parses it
|
45
|
+
# into an Object. Defaults to the standard library's JSON.parse method.
|
46
|
+
attr_accessor :json_decoder
|
47
|
+
|
48
|
+
# @returns [Logger]
|
49
|
+
attr_accessor :logger
|
50
|
+
|
51
|
+
# @return [Integer] maximum number of timing measurements to keep in memory
|
52
|
+
# before compressing them.
|
53
|
+
attr_accessor :max_performance_samples
|
54
|
+
|
55
|
+
# @return [Integer] maximum number of compressed performance samples to keep
|
56
|
+
# in memory. If we take more than this before reporting them to Aikido, we
|
57
|
+
# will discard the oldest samples.
|
58
|
+
attr_accessor :max_compressed_stats
|
59
|
+
|
60
|
+
# @return [Integer] maximum number of connections to outbound hosts to keep
|
61
|
+
# in memory in order to report them in the next heartbeat event. If new
|
62
|
+
# connections are added to the set before reporting them to Aikido, we
|
63
|
+
# will discard the oldest data point.
|
64
|
+
attr_accessor :max_outbound_connections
|
65
|
+
|
66
|
+
# @return [Integer] maximum number of users tracked via Zen.track_user to
|
67
|
+
# share with the Aikido servers on the next heartbeat event. If more
|
68
|
+
# unique users (by their ID) are tracked than this number, we will discard
|
69
|
+
# the oldest seen users.
|
70
|
+
attr_accessor :max_users_tracked
|
71
|
+
|
72
|
+
# @return [Proc{Aikido::Zen::Request => Array(Integer, Hash, #each)}]
|
73
|
+
# Rack handler used to respond to requests from IPs blocked in the Aikido
|
74
|
+
# dashboard.
|
75
|
+
attr_accessor :blocked_ip_responder
|
76
|
+
|
77
|
+
# @return [Proc{Aikido::Zen::Request => Array(Integer, Hash, #each)}]
|
78
|
+
# Rack handler used to respond to requests that have been rate limited.
|
79
|
+
attr_accessor :rate_limited_responder
|
80
|
+
|
81
|
+
# @return [Proc{Aikido::Zen::Request => String}] a proc that reads
|
82
|
+
# information off the current request and returns a String to
|
83
|
+
# differentiate different clients. By default this uses the request IP.
|
84
|
+
attr_accessor :rate_limiting_discriminator
|
85
|
+
|
86
|
+
# @return [Boolean] whether Zen should infer the schema from request bodies
|
87
|
+
# sent to the app. Defaults to +false+ or the value of the environment
|
88
|
+
# variable AIKIDO_FEATURE_COLLECT_API_SCHEMA.
|
89
|
+
attr_accessor :api_schema_collection_enabled
|
90
|
+
alias_method :api_schema_collection_enabled?, :api_schema_collection_enabled
|
91
|
+
|
92
|
+
# @api private
|
93
|
+
# @return [Integer] max number of levels deep we want to read a nested
|
94
|
+
# strcture for performance reasons.
|
95
|
+
attr_accessor :api_schema_collection_max_depth
|
96
|
+
|
97
|
+
# @api private
|
98
|
+
# @return [Integer] max number of properties that we want to inspect per
|
99
|
+
# level of the structure for performance reasons.
|
100
|
+
attr_accessor :api_schema_collection_max_properties
|
101
|
+
|
102
|
+
# @api private
|
103
|
+
# @return [Proc<Hash => Aikido::Zen::Context>] callable that takes a
|
104
|
+
# Rack-compatible env Hash and returns a Context object with an HTTP
|
105
|
+
# request. This is meant to be overridden by each framework adapter.
|
106
|
+
attr_accessor :request_builder
|
107
|
+
|
108
|
+
# @api private
|
109
|
+
# @return [Integer] number of seconds to perform client-side rate limiting
|
110
|
+
# of events sent to the server.
|
111
|
+
attr_accessor :client_rate_limit_period
|
112
|
+
|
113
|
+
# @api private
|
114
|
+
# @return [Integer] max number of events sent during a sliding
|
115
|
+
# {client_rate_limit_period} window.
|
116
|
+
attr_accessor :client_rate_limit_max_events
|
117
|
+
|
118
|
+
# @api private
|
119
|
+
# @return [Integer] number of seconds to wait before sending an event after
|
120
|
+
# the server returns a 429 response.
|
121
|
+
attr_accessor :server_rate_limit_deadline
|
122
|
+
|
123
|
+
# @return [Array<String>] when checking for stored SSRF attacks, we want to
|
124
|
+
# allow known hosts that should be able to resolve to the IMDS service.
|
125
|
+
attr_accessor :imds_allowed_hosts
|
126
|
+
|
127
|
+
def initialize
|
128
|
+
self.blocking_mode = !!ENV.fetch("AIKIDO_BLOCKING", false)
|
129
|
+
self.api_timeouts = 10
|
130
|
+
self.api_base_url = ENV.fetch("AIKIDO_BASE_URL", DEFAULT_API_BASE_URL)
|
131
|
+
self.runtime_api_base_url = ENV.fetch("AIKIDO_RUNTIME_URL", DEFAULT_RUNTIME_BASE_URL)
|
132
|
+
self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
|
133
|
+
self.polling_interval = 60
|
134
|
+
self.initial_heartbeat_delay = 60
|
135
|
+
self.json_encoder = DEFAULT_JSON_ENCODER
|
136
|
+
self.json_decoder = DEFAULT_JSON_DECODER
|
137
|
+
self.logger = Logger.new($stdout, progname: "aikido")
|
138
|
+
self.max_performance_samples = 5000
|
139
|
+
self.max_compressed_stats = 100
|
140
|
+
self.max_outbound_connections = 200
|
141
|
+
self.max_users_tracked = 1000
|
142
|
+
self.request_builder = Aikido::Zen::Context::RACK_REQUEST_BUILDER
|
143
|
+
self.blocked_ip_responder = DEFAULT_BLOCKED_IP_RESPONDER
|
144
|
+
self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
|
145
|
+
self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
|
146
|
+
self.server_rate_limit_deadline = 1800 # 30 min
|
147
|
+
self.client_rate_limit_period = 3600 # 1 hour
|
148
|
+
self.client_rate_limit_max_events = 100
|
149
|
+
|
150
|
+
self.api_schema_collection_enabled = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", false))
|
151
|
+
self.api_schema_collection_max_depth = 20
|
152
|
+
self.api_schema_collection_max_properties = 20
|
153
|
+
|
154
|
+
self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
|
155
|
+
end
|
156
|
+
|
157
|
+
# Set the base URL for API requests.
|
158
|
+
#
|
159
|
+
# @param url [String, URI]
|
160
|
+
def api_base_url=(url)
|
161
|
+
@api_base_url = URI(url)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Set the base URL for runtime API requests.
|
165
|
+
#
|
166
|
+
# @param url [String, URI]
|
167
|
+
def runtime_api_base_url=(url)
|
168
|
+
@runtime_api_base_url = URI(url)
|
169
|
+
end
|
170
|
+
|
171
|
+
# @overload def api_timeouts=(timeouts)
|
172
|
+
# Configure granular connection timeouts for the Aikido Zen API. You
|
173
|
+
# can set any of these per call.
|
174
|
+
# @param timeouts [Hash]
|
175
|
+
# @option timeouts [Integer] :open_timeout Duration in seconds.
|
176
|
+
# @option timeouts [Integer] :read_timeout Duration in seconds.
|
177
|
+
# @option timeouts [Integer] :write_timeout Duration in seconds.
|
178
|
+
#
|
179
|
+
# @overload def api_timeouts=(duration)
|
180
|
+
# Configure the connection timeouts for the Aikido Zen API.
|
181
|
+
# @param duration [Integer] Duration in seconds to set for all three
|
182
|
+
# timeouts (open, read, and write).
|
183
|
+
def api_timeouts=(value)
|
184
|
+
value = {open_timeout: value, read_timeout: value, write_timeout: value} if value.respond_to?(:to_int)
|
185
|
+
|
186
|
+
@api_timeouts ||= {}
|
187
|
+
@api_timeouts.update(value)
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
def read_boolean_from_env(value)
|
193
|
+
return value unless value.respond_to?(:to_str)
|
194
|
+
|
195
|
+
case value.to_str.strip
|
196
|
+
when "false", "", "0", "f"
|
197
|
+
false
|
198
|
+
else
|
199
|
+
true
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# @!visibility private
|
204
|
+
DEFAULT_API_BASE_URL = "https://guard.aikido.dev"
|
205
|
+
|
206
|
+
# @!visibility private
|
207
|
+
DEFAULT_RUNTIME_BASE_URL = "https://runtime.aikido.dev"
|
208
|
+
|
209
|
+
# @!visibility private
|
210
|
+
DEFAULT_JSON_ENCODER = JSON.method(:dump)
|
211
|
+
|
212
|
+
# @!visibility private
|
213
|
+
DEFAULT_JSON_DECODER = JSON.method(:parse)
|
214
|
+
|
215
|
+
# @!visibility private
|
216
|
+
DEFAULT_BLOCKED_IP_RESPONDER = ->(request) do
|
217
|
+
message = "Your IP address is not allowed to access this resource. (Your IP: %s)"
|
218
|
+
[403, {"Content-Type" => "text/plain"}, [format(message, request.ip)]]
|
219
|
+
end
|
220
|
+
|
221
|
+
# @!visibility private
|
222
|
+
DEFAULT_RATE_LIMITED_RESPONDER = ->(request) do
|
223
|
+
[429, {"Content-Type" => "text/plain"}, ["Too many requests."]]
|
224
|
+
end
|
225
|
+
|
226
|
+
# @!visibility private
|
227
|
+
DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) { request.ip }
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../request"
|
4
|
+
require_relative "../request/heuristic_router"
|
5
|
+
|
6
|
+
module Aikido::Zen
|
7
|
+
# @!visibility private
|
8
|
+
Context::RACK_REQUEST_BUILDER = ->(env) do
|
9
|
+
delegate = Rack::Request.new(env)
|
10
|
+
router = Aikido::Zen::Request::HeuristicRouter.new
|
11
|
+
request = Aikido::Zen::Request.new(delegate, framework: "rack", router: router)
|
12
|
+
|
13
|
+
Context.new(request) do |req|
|
14
|
+
{
|
15
|
+
query: req.GET,
|
16
|
+
body: req.POST,
|
17
|
+
route: {},
|
18
|
+
header: req.normalized_headers,
|
19
|
+
cookie: req.cookies,
|
20
|
+
subdomain: []
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../request"
|
4
|
+
require_relative "../request/rails_router"
|
5
|
+
|
6
|
+
module Aikido::Zen
|
7
|
+
module Rails
|
8
|
+
def self.router
|
9
|
+
@router ||= Request::RailsRouter.new(::Rails.application.routes)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# @!visibility private
|
14
|
+
Context::RAILS_REQUEST_BUILDER = ->(env) do
|
15
|
+
delegate = ActionDispatch::Request.new(env)
|
16
|
+
request = Aikido::Zen::Request.new(
|
17
|
+
delegate, framework: "rails", router: Rails.router
|
18
|
+
)
|
19
|
+
|
20
|
+
decrypt_cookies = ->(req) do
|
21
|
+
return req.cookies unless req.respond_to?(:cookie_jar)
|
22
|
+
|
23
|
+
req.cookie_jar.map { |key, value|
|
24
|
+
plain_text = req.cookie_jar.encrypted[key].presence ||
|
25
|
+
req.cookie_jar.signed[key].presence ||
|
26
|
+
value
|
27
|
+
[key, plain_text]
|
28
|
+
}.to_h
|
29
|
+
end
|
30
|
+
|
31
|
+
Context.new(request) do |req|
|
32
|
+
{
|
33
|
+
query: req.query_parameters,
|
34
|
+
body: req.request_parameters,
|
35
|
+
route: req.path_parameters,
|
36
|
+
header: req.normalized_headers,
|
37
|
+
cookie: decrypt_cookies.call(req),
|
38
|
+
subdomain: req.subdomains
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,101 @@
|
|
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
|
+
def self.from_rack_env(env, config = Aikido::Zen.config)
|
11
|
+
config.request_builder.call(env)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :request
|
15
|
+
|
16
|
+
# @param [Rack::Request] a Request object that implements the
|
17
|
+
# Rack::Request API, to which we will delegate behavior.
|
18
|
+
# @param settings [Aikido::Zen::RuntimeSettings]
|
19
|
+
#
|
20
|
+
# @yieldparam request [Rack::Request] the given request object.
|
21
|
+
# @yieldreturn [Hash<Symbol, #flat_map>] map of payload source types
|
22
|
+
# to the actual data from the request to populate them.
|
23
|
+
def initialize(request, settings: Aikido::Zen.runtime_settings, &sources)
|
24
|
+
@request = request
|
25
|
+
@settings = settings
|
26
|
+
@payload_sources = sources
|
27
|
+
@metadata = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Fetch some metadata stored in the Context.
|
31
|
+
#
|
32
|
+
# @param key [String]
|
33
|
+
# @return [Object, nil]
|
34
|
+
def [](key)
|
35
|
+
@metadata[key]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Store some metadata in the Context so other Scanners can use it.
|
39
|
+
#
|
40
|
+
# @param key [String]
|
41
|
+
# @param value [Object]
|
42
|
+
# @return [void]
|
43
|
+
def []=(key, value)
|
44
|
+
@metadata[key] = value
|
45
|
+
end
|
46
|
+
|
47
|
+
# Overrides the current request, and invalidates any memoized data obtained
|
48
|
+
# from it. This is useful for scenarios where setting the request in the
|
49
|
+
# middleware isn't enough, such as Rails, where the router modifies it after
|
50
|
+
# the middleware has seen it.
|
51
|
+
#
|
52
|
+
# @param new_request [Rack::Request]
|
53
|
+
# @return [void]
|
54
|
+
def update_request(new_request)
|
55
|
+
@payloads = nil
|
56
|
+
request.__setobj__(new_request)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [Array<Aikido::Zen::Payload>] list of user inputs from all the
|
60
|
+
# different sources we recognize.
|
61
|
+
def payloads
|
62
|
+
@payloads ||= payload_sources.flat_map do |source, data|
|
63
|
+
extract_payloads_from(data, source)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Boolean] whether attack protection for the currently requested
|
68
|
+
# endpoint was disabled on the Aikido dashboard, or if the source IP for
|
69
|
+
# this request is in the "Bypass List".
|
70
|
+
def protection_disabled?
|
71
|
+
return false if request.nil?
|
72
|
+
|
73
|
+
!@settings.endpoints[request.route].protected? ||
|
74
|
+
@settings.skip_protection_for_ips.include?(request.ip)
|
75
|
+
end
|
76
|
+
|
77
|
+
# @!visibility private
|
78
|
+
def payload_sources
|
79
|
+
@payload_sources.call(request)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def extract_payloads_from(data, source_type, prefix = nil)
|
85
|
+
if data.respond_to?(:to_hash)
|
86
|
+
data.to_hash.flat_map { |name, val|
|
87
|
+
extract_payloads_from(val, source_type, [prefix, name].compact.join("."))
|
88
|
+
}
|
89
|
+
elsif data.respond_to?(:to_ary)
|
90
|
+
data.to_ary.flat_map.with_index { |val, idx|
|
91
|
+
extract_payloads_from(val, source_type, [prefix, idx].compact.join("."))
|
92
|
+
}
|
93
|
+
else
|
94
|
+
Payload.new(data, source_type, prefix.to_s)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
require_relative "context/rack_request"
|
101
|
+
require_relative "context/rails_request"
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Aikido
|
6
|
+
# Support rescuing Aikido::Error without forcing a single base class to all
|
7
|
+
# errors (so things that should be e.g. a TypeError, can have the correct
|
8
|
+
# superclass).
|
9
|
+
module Error; end
|
10
|
+
|
11
|
+
# Generic error for problems with the Agent.
|
12
|
+
class ZenError < RuntimeError
|
13
|
+
include Error
|
14
|
+
end
|
15
|
+
|
16
|
+
module Zen
|
17
|
+
# Wrapper for all low-level network errors communicating with the API. You
|
18
|
+
# can access the original error by calling #cause.
|
19
|
+
class NetworkError < StandardError
|
20
|
+
include Error
|
21
|
+
|
22
|
+
def initialize(request, cause = nil)
|
23
|
+
@request = request.dup
|
24
|
+
|
25
|
+
super("Error in #{request.method} #{request.path}: #{cause.message}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Raised whenever a request to the API results in a 4XX or 5XX response.
|
30
|
+
class APIError < StandardError
|
31
|
+
include Error
|
32
|
+
|
33
|
+
attr_reader :request
|
34
|
+
attr_reader :response
|
35
|
+
|
36
|
+
def initialize(request, response)
|
37
|
+
@request = anonimize_token(request.dup)
|
38
|
+
@response = response
|
39
|
+
|
40
|
+
super("Error in #{request.method} #{request.path}: #{response.code} #{response.message} (#{response.body})")
|
41
|
+
end
|
42
|
+
|
43
|
+
private def anonimize_token(request)
|
44
|
+
# Anonimize the token to `********************xxxx`,
|
45
|
+
# mimicking what we show in the dashbaord.
|
46
|
+
request["Authorization"] = request["Authorization"].to_s
|
47
|
+
.gsub(/\A.*(.{4})\z/, ("*" * 20) + "\\1")
|
48
|
+
request
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Raised whenever a response to the API results in a 429 response.
|
53
|
+
class RateLimitedError < APIError; end
|
54
|
+
|
55
|
+
class UnderAttackError < StandardError
|
56
|
+
include Error
|
57
|
+
|
58
|
+
attr_reader :attack
|
59
|
+
|
60
|
+
def initialize(attack)
|
61
|
+
super(attack.log_message)
|
62
|
+
@attack = attack
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class SQLInjectionError < UnderAttackError
|
67
|
+
extend Forwardable
|
68
|
+
def_delegators :@attack, :query, :input, :dialect
|
69
|
+
end
|
70
|
+
|
71
|
+
class SSRFDetectedError < UnderAttackError
|
72
|
+
extend Forwardable
|
73
|
+
def_delegators :@attack, :request, :input
|
74
|
+
end
|
75
|
+
|
76
|
+
# Raised when there's any problem communicating (or loading) libzen.
|
77
|
+
class InternalsError < ZenError
|
78
|
+
# @param attempt [String] description of what we were trying to do.
|
79
|
+
# @param problem [String] what couldn't be done.
|
80
|
+
# @param libname [String] the name of the file (including the arch).
|
81
|
+
def initialize(attempt, problem, libname)
|
82
|
+
super(format(<<~MSG.chomp, attempt, problem, libname))
|
83
|
+
Zen could not scan %s due to a problem %s the library `%s'
|
84
|
+
MSG
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen
|
4
|
+
# Base class for all events. You should be using one of the subclasses defined
|
5
|
+
# in the Events module.
|
6
|
+
class Event
|
7
|
+
attr_reader :type
|
8
|
+
attr_reader :time
|
9
|
+
attr_reader :system_info
|
10
|
+
|
11
|
+
def initialize(type:, system_info: Aikido::Zen.system_info, time: Time.now.utc)
|
12
|
+
@type = type
|
13
|
+
@time = time
|
14
|
+
@system_info = system_info
|
15
|
+
end
|
16
|
+
|
17
|
+
def as_json
|
18
|
+
{
|
19
|
+
type: type,
|
20
|
+
time: time.to_i * 1000,
|
21
|
+
agent: system_info.as_json
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Events
|
27
|
+
# Event sent when starting up the agent.
|
28
|
+
class Started < Event
|
29
|
+
def initialize(**opts)
|
30
|
+
super(type: "started", **opts)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Attack < Event
|
35
|
+
attr_reader :attack
|
36
|
+
|
37
|
+
def initialize(attack:, **opts)
|
38
|
+
@attack = attack
|
39
|
+
super(type: "detected_attack", **opts)
|
40
|
+
end
|
41
|
+
|
42
|
+
def as_json
|
43
|
+
super.update(
|
44
|
+
attack: @attack.as_json,
|
45
|
+
request: @attack.context.request.as_json
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Heartbeat < Event
|
51
|
+
def initialize(stats:, **opts)
|
52
|
+
super(type: "heartbeat", **opts)
|
53
|
+
@stats = stats
|
54
|
+
end
|
55
|
+
|
56
|
+
def as_json
|
57
|
+
super.update(
|
58
|
+
stats: @stats.as_json,
|
59
|
+
routes: @stats.routes.as_json,
|
60
|
+
hostnames: @stats.outbound_connections.as_json,
|
61
|
+
users: @stats.users.as_json
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
require_relative "errors"
|
5
|
+
|
6
|
+
module Aikido::Zen
|
7
|
+
module Internals
|
8
|
+
extend FFI::Library
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# @return [String] the name of the extension we're loading, which we can
|
12
|
+
# use in error messages to identify the architecture.
|
13
|
+
attr_accessor :libzen_name
|
14
|
+
end
|
15
|
+
|
16
|
+
self.libzen_name = [
|
17
|
+
"libzen-v#{LIBZEN_VERSION}",
|
18
|
+
FFI::Platform::ARCH,
|
19
|
+
FFI::Platform::LIBSUFFIX
|
20
|
+
].join(".")
|
21
|
+
|
22
|
+
begin
|
23
|
+
ffi_lib File.expand_path(libzen_name, __dir__)
|
24
|
+
|
25
|
+
# @!method self.detect_sql_injection_native(query, input, dialect)
|
26
|
+
# @param (see .detect_sql_injection)
|
27
|
+
# @returns [Integer] 0 if no injection detected, 1 if an injection was
|
28
|
+
# detected, or 2 if there was an internal error.
|
29
|
+
# @raise [Aikido::Zen::InternalsError] if there's a problem loading or
|
30
|
+
# calling libzen.
|
31
|
+
attach_function :detect_sql_injection_native, :detect_sql_injection,
|
32
|
+
[:string, :string, :int], :int
|
33
|
+
rescue LoadError, FFI::NotFoundError => err
|
34
|
+
# Emit an $stderr warning at startup.
|
35
|
+
warn "Zen could not load its binary extension #{libzen_name}: #{err}"
|
36
|
+
|
37
|
+
def self.detect_sql_injection(query, *)
|
38
|
+
attempt = format("%p for SQL injection", query)
|
39
|
+
raise InternalsError.new(attempt, "loading", Internals.libzen_name)
|
40
|
+
end
|
41
|
+
else
|
42
|
+
# Analyzes the SQL query to detect if the provided user input is being
|
43
|
+
# passed as-is without escaping.
|
44
|
+
#
|
45
|
+
# @param query [String]
|
46
|
+
# @param input [String]
|
47
|
+
# @param dialect [Integer, #to_int] the SQL Dialect identifier in libzen.
|
48
|
+
# See {Aikido::Zen::Scanners::SQLInjectionScanner::DIALECTS}.
|
49
|
+
#
|
50
|
+
# @returns [Boolean]
|
51
|
+
# @raise [Aikido::Zen::InternalsError] if there's a problem loading or
|
52
|
+
# calling libzen.
|
53
|
+
def self.detect_sql_injection(query, input, dialect)
|
54
|
+
case detect_sql_injection_native(query, input, dialect)
|
55
|
+
when 0 then false
|
56
|
+
when 1 then true
|
57
|
+
when 2
|
58
|
+
attempt = format("%s query %p with input %p", dialect, query, input)
|
59
|
+
raise InternalsError.new(attempt, "calling detect_sql_injection in", libzen_name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
Binary file
|