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,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen
|
|
4
|
+
module Middleware
|
|
5
|
+
# Rack middleware used to track request
|
|
6
|
+
# It implements the logic under that which is considered worthy of being tracked.
|
|
7
|
+
class RequestTracker
|
|
8
|
+
def initialize(app, settings: Aikido::Zen.runtime_settings)
|
|
9
|
+
@app = app
|
|
10
|
+
@settings = settings
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
request = Aikido::Zen::Middleware.request_from(env)
|
|
15
|
+
response = @app.call(env)
|
|
16
|
+
|
|
17
|
+
if request.route && track?(
|
|
18
|
+
status_code: response[0],
|
|
19
|
+
route: request.route.path,
|
|
20
|
+
http_method: request.request_method,
|
|
21
|
+
ip: request.ip
|
|
22
|
+
)
|
|
23
|
+
Aikido::Zen.track_request(request)
|
|
24
|
+
|
|
25
|
+
if Aikido::Zen.config.collect_api_schema?
|
|
26
|
+
Aikido::Zen.track_discovered_route(request)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
response
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
IGNORED_METHODS = %w[OPTIONS HEAD]
|
|
34
|
+
IGNORED_EXTENSIONS = %w[properties config webmanifest]
|
|
35
|
+
IGNORED_SEGMENTS = ["cgi-bin"]
|
|
36
|
+
WELL_KNOWN_URIS = %w[
|
|
37
|
+
/.well-known/acme-challenge
|
|
38
|
+
/.well-known/amphtml
|
|
39
|
+
/.well-known/api-catalog
|
|
40
|
+
/.well-known/appspecific
|
|
41
|
+
/.well-known/ashrae
|
|
42
|
+
/.well-known/assetlinks.json
|
|
43
|
+
/.well-known/broadband-labels
|
|
44
|
+
/.well-known/brski
|
|
45
|
+
/.well-known/caldav
|
|
46
|
+
/.well-known/carddav
|
|
47
|
+
/.well-known/change-password
|
|
48
|
+
/.well-known/cmp
|
|
49
|
+
/.well-known/coap
|
|
50
|
+
/.well-known/coap-eap
|
|
51
|
+
/.well-known/core
|
|
52
|
+
/.well-known/csaf
|
|
53
|
+
/.well-known/csaf-aggregator
|
|
54
|
+
/.well-known/csvm
|
|
55
|
+
/.well-known/did.json
|
|
56
|
+
/.well-known/did-configuration.json
|
|
57
|
+
/.well-known/dnt
|
|
58
|
+
/.well-known/dnt-policy.txt
|
|
59
|
+
/.well-known/dots
|
|
60
|
+
/.well-known/ecips
|
|
61
|
+
/.well-known/edhoc
|
|
62
|
+
/.well-known/enterprise-network-security
|
|
63
|
+
/.well-known/enterprise-transport-security
|
|
64
|
+
/.well-known/est
|
|
65
|
+
/.well-known/genid
|
|
66
|
+
/.well-known/gnap-as-rs
|
|
67
|
+
/.well-known/gpc.json
|
|
68
|
+
/.well-known/gs1resolver
|
|
69
|
+
/.well-known/hoba
|
|
70
|
+
/.well-known/host-meta
|
|
71
|
+
/.well-known/host-meta.json
|
|
72
|
+
/.well-known/hosting-provider
|
|
73
|
+
/.well-known/http-opportunistic
|
|
74
|
+
/.well-known/idp-proxy
|
|
75
|
+
/.well-known/jmap
|
|
76
|
+
/.well-known/keybase.txt
|
|
77
|
+
/.well-known/knx
|
|
78
|
+
/.well-known/looking-glass
|
|
79
|
+
/.well-known/masque
|
|
80
|
+
/.well-known/matrix
|
|
81
|
+
/.well-known/mercure
|
|
82
|
+
/.well-known/mta-sts.txt
|
|
83
|
+
/.well-known/mud
|
|
84
|
+
/.well-known/nfv-oauth-server-configuration
|
|
85
|
+
/.well-known/ni
|
|
86
|
+
/.well-known/nodeinfo
|
|
87
|
+
/.well-known/nostr.json
|
|
88
|
+
/.well-known/oauth-authorization-server
|
|
89
|
+
/.well-known/oauth-protected-resource
|
|
90
|
+
/.well-known/ohttp-gateway
|
|
91
|
+
/.well-known/openid-federation
|
|
92
|
+
/.well-known/open-resource-discovery
|
|
93
|
+
/.well-known/openid-configuration
|
|
94
|
+
/.well-known/openorg
|
|
95
|
+
/.well-known/oslc
|
|
96
|
+
/.well-known/pki-validation
|
|
97
|
+
/.well-known/posh
|
|
98
|
+
/.well-known/privacy-sandbox-attestations.json
|
|
99
|
+
/.well-known/private-token-issuer-directory
|
|
100
|
+
/.well-known/probing.txt
|
|
101
|
+
/.well-known/pvd
|
|
102
|
+
/.well-known/rd
|
|
103
|
+
/.well-known/related-website-set.json
|
|
104
|
+
/.well-known/reload-config
|
|
105
|
+
/.well-known/repute-template
|
|
106
|
+
/.well-known/resourcesync
|
|
107
|
+
/.well-known/sbom
|
|
108
|
+
/.well-known/security.txt
|
|
109
|
+
/.well-known/ssf-configuration
|
|
110
|
+
/.well-known/sshfp
|
|
111
|
+
/.well-known/stun-key
|
|
112
|
+
/.well-known/terraform.json
|
|
113
|
+
/.well-known/thread
|
|
114
|
+
/.well-known/time
|
|
115
|
+
/.well-known/timezone
|
|
116
|
+
/.well-known/tdmrep.json
|
|
117
|
+
/.well-known/tor-relay
|
|
118
|
+
/.well-known/tpcd
|
|
119
|
+
/.well-known/traffic-advice
|
|
120
|
+
/.well-known/trust.txt
|
|
121
|
+
/.well-known/uma2-configuration
|
|
122
|
+
/.well-known/void
|
|
123
|
+
/.well-known/webfinger
|
|
124
|
+
/.well-known/webweaver.json
|
|
125
|
+
/.well-known/wot
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# @param status_code [Integer]
|
|
129
|
+
# @param route [String]
|
|
130
|
+
# @param http_method [String]
|
|
131
|
+
def track?(status_code:, route:, http_method:, ip: nil)
|
|
132
|
+
# Bypass request and route tracking for allowed IPs
|
|
133
|
+
return false if @settings.allowed_ips.include?(ip)
|
|
134
|
+
|
|
135
|
+
# In the UI we want to show only successful (2xx) or redirect (3xx) responses
|
|
136
|
+
# anything else is discarded.
|
|
137
|
+
return false unless status_code >= 200 && status_code <= 399
|
|
138
|
+
|
|
139
|
+
return false if IGNORED_METHODS.include?(http_method)
|
|
140
|
+
|
|
141
|
+
segments = route.split "/"
|
|
142
|
+
|
|
143
|
+
# Do not discover routes with dot files like `/path/to/.file` or `/.directory/file`
|
|
144
|
+
# We want to allow discovery of well-known URIs like `/.well-known/acme-challenge`
|
|
145
|
+
return false if segments.any? { |s| is_dot_file s } && !is_well_known_uri(route)
|
|
146
|
+
|
|
147
|
+
return false if segments.any? { |s| contains_ignored_string s }
|
|
148
|
+
|
|
149
|
+
# Check for every file segment if it contains a file extension and if it
|
|
150
|
+
# should be discovered or ignored
|
|
151
|
+
segments.all? { |s| should_track_extension s }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
# Check if a path is a well-known URI
|
|
157
|
+
# e.g. /.well-known/acme-challenge
|
|
158
|
+
# https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml
|
|
159
|
+
def is_well_known_uri(route)
|
|
160
|
+
WELL_KNOWN_URIS.include?(route)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def is_dot_file(segment)
|
|
164
|
+
segment.start_with?(".") && segment.size > 1
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def contains_ignored_string(segment)
|
|
168
|
+
IGNORED_SEGMENTS.any? { |ignored| segment.include?(ignored) }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Ignore routes which contain file extensions
|
|
172
|
+
def should_track_extension(segment)
|
|
173
|
+
extension = get_file_extension(segment)
|
|
174
|
+
|
|
175
|
+
return true unless extension
|
|
176
|
+
|
|
177
|
+
# Do not discover files with extensions of 1 to 5 characters,
|
|
178
|
+
# e.g. file.css, file.js, file.woff2
|
|
179
|
+
return false if extension.size > 1 && extension.size < 6
|
|
180
|
+
|
|
181
|
+
# Ignore some file extensions that are longer than 5 characters or shorter than 2 chars
|
|
182
|
+
return false if IGNORED_EXTENSIONS.include?(extension)
|
|
183
|
+
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def get_file_extension(segment)
|
|
188
|
+
extension = File.extname(segment)
|
|
189
|
+
if extension&.start_with?(".")
|
|
190
|
+
# Remove the dot from the extension
|
|
191
|
+
return extension[1..]
|
|
192
|
+
end
|
|
193
|
+
extension
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen
|
|
4
|
+
# Simple data object to identify connections performed to outbound servers.
|
|
5
|
+
class OutboundConnection
|
|
6
|
+
def self.from_json(data)
|
|
7
|
+
new(
|
|
8
|
+
host: data[:hostname],
|
|
9
|
+
port: data[:port]
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Convenience factory to create connection descriptions out of URI objects.
|
|
14
|
+
#
|
|
15
|
+
# @param uri [URI]
|
|
16
|
+
# @return [Aikido::Zen::OutboundConnection]
|
|
17
|
+
def self.from_uri(uri)
|
|
18
|
+
new(host: uri.hostname, port: uri.port)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [String] the hostname or IP address to which the connection was
|
|
22
|
+
# attempted.
|
|
23
|
+
attr_reader :host
|
|
24
|
+
|
|
25
|
+
# @return [Integer] the port number to which the connection was attempted.
|
|
26
|
+
attr_reader :port
|
|
27
|
+
|
|
28
|
+
# @return [Integer] the number of times that this connection was seen by
|
|
29
|
+
# the hosts collector.
|
|
30
|
+
attr_reader :hits
|
|
31
|
+
|
|
32
|
+
def initialize(host:, port:)
|
|
33
|
+
@host = host
|
|
34
|
+
@port = port
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def hit
|
|
38
|
+
# Lazy initialize @hits, so it stays nil until the connection is tracked.
|
|
39
|
+
@hits ||= 0
|
|
40
|
+
@hits += 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def as_json
|
|
44
|
+
{hostname: host, port: port, hits: hits}.compact
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ==(other)
|
|
48
|
+
other.is_a?(OutboundConnection) &&
|
|
49
|
+
host == other.host &&
|
|
50
|
+
port == other.port
|
|
51
|
+
end
|
|
52
|
+
alias_method :eql?, :==
|
|
53
|
+
|
|
54
|
+
def hash
|
|
55
|
+
[host, port].hash
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def inspect
|
|
59
|
+
"#<#{self.class.name} #{host}:#{port}>"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen
|
|
4
|
+
# This simple callable follows the Scanner API so that it can be injected into
|
|
5
|
+
# any Sink that wraps an HTTP library, and lets us keep track of any hosts to
|
|
6
|
+
# which the app communicates over HTTP.
|
|
7
|
+
module OutboundConnectionMonitor
|
|
8
|
+
def self.skips_on_nil_context?
|
|
9
|
+
false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# This simply reports the connection to the Agent, and always returns +nil+
|
|
13
|
+
# as it's not scanning for any particular attack.
|
|
14
|
+
#
|
|
15
|
+
# @param connection [Aikido::Zen::OutboundConnection]
|
|
16
|
+
# @return [nil]
|
|
17
|
+
def self.call(connection:, **)
|
|
18
|
+
Aikido::Zen.track_outbound(connection)
|
|
19
|
+
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sink"
|
|
4
|
+
|
|
5
|
+
module Aikido::Zen
|
|
6
|
+
Package = Struct.new(:name, :version) do
|
|
7
|
+
def initialize(name, version, sinks = Aikido::Zen::Sinks.registry)
|
|
8
|
+
super(name, version)
|
|
9
|
+
@sinks = sinks
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [Boolean] whether we explicitly protect against exploits in this
|
|
13
|
+
# library.
|
|
14
|
+
def supported?
|
|
15
|
+
@sinks.include?(name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def as_json
|
|
19
|
+
{name => version.to_s}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen
|
|
4
|
+
# An individual user input in a request, which may come from different
|
|
5
|
+
# sources (query string, body, cookies, etc).
|
|
6
|
+
class Payload
|
|
7
|
+
attr_reader :value, :source, :path
|
|
8
|
+
|
|
9
|
+
def initialize(value, source, path)
|
|
10
|
+
@value = value
|
|
11
|
+
@source = source
|
|
12
|
+
@path = path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
UNKNOWN_PAYLOAD = Payload.new("unknown", "unknown", "unknown")
|
|
16
|
+
|
|
17
|
+
alias_method :to_s, :value
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
other.is_a?(Payload) &&
|
|
21
|
+
other.value == value &&
|
|
22
|
+
other.source == source &&
|
|
23
|
+
other.path == path
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def as_json
|
|
27
|
+
{
|
|
28
|
+
payload: value.to_s,
|
|
29
|
+
source: SOURCE_SERIALIZATIONS[source],
|
|
30
|
+
path: path.to_s
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
SOURCE_SERIALIZATIONS = {
|
|
35
|
+
query: "query",
|
|
36
|
+
body: "body",
|
|
37
|
+
header: "headers",
|
|
38
|
+
cookie: "cookies",
|
|
39
|
+
route: "routeParams",
|
|
40
|
+
graphql: "graphql",
|
|
41
|
+
xml: "xml",
|
|
42
|
+
subdomain: "subdomains"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def inspect
|
|
46
|
+
val = (value.to_s.size > 128) ? value[0..125] + "..." : value
|
|
47
|
+
"#<Aikido::Zen::Payload #{source}(#{path}) #{val.inspect}>"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_dispatch"
|
|
4
|
+
|
|
5
|
+
module Aikido::Zen
|
|
6
|
+
class RailsEngine < ::Rails::Engine
|
|
7
|
+
config.before_configuration do
|
|
8
|
+
# Access library configuration at `Rails.application.config.zen`.
|
|
9
|
+
config.zen = Aikido::Zen.config
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
initializer "aikido.add_middleware" do |app|
|
|
13
|
+
app.middleware.insert_before 0, Aikido::Zen::Middleware::ForkDetector
|
|
14
|
+
|
|
15
|
+
app.middleware.use Aikido::Zen::Middleware::ContextSetter
|
|
16
|
+
app.middleware.use Aikido::Zen::Middleware::AllowedAddressChecker
|
|
17
|
+
app.middleware.use Aikido::Zen::Middleware::AttackWaveProtector
|
|
18
|
+
# Request Tracker stats do not consider failed request or 40x, so the middleware
|
|
19
|
+
# must be the last one wrapping the request.
|
|
20
|
+
app.middleware.use Aikido::Zen::Middleware::RequestTracker
|
|
21
|
+
|
|
22
|
+
ActiveSupport.on_load(:action_controller) do
|
|
23
|
+
# Due to how Rails sets up its middleware chain, the routing is evaluated
|
|
24
|
+
# (and the Request object constructed) in the app that terminates the
|
|
25
|
+
# chain, so no amount of middleware will be able to access it.
|
|
26
|
+
#
|
|
27
|
+
# This way, we overwrite the Request object as early as we can in the
|
|
28
|
+
# request handling, so that by the time we start evaluating inputs, we
|
|
29
|
+
# have assigned the request correctly.
|
|
30
|
+
before_action { Aikido::Zen.current_context.update_request(request) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
initializer "aikido.configuration" do |app|
|
|
35
|
+
app.config.zen.request_builder = Aikido::Zen::Context::RAILS_REQUEST_BUILDER
|
|
36
|
+
|
|
37
|
+
# Plug Rails' JSON encoder/decoder, but only if the user hasn't changed
|
|
38
|
+
# them for something else.
|
|
39
|
+
if app.config.zen.json_encoder == Aikido::Zen::Config::DEFAULT_JSON_ENCODER
|
|
40
|
+
app.config.zen.json_encoder = ActiveSupport::JSON.method(:encode)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if app.config.zen.json_decoder == Aikido::Zen::Config::DEFAULT_JSON_DECODER
|
|
44
|
+
app.config.zen.json_decoder = ActiveSupport::JSON.method(:decode)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
config.after_initialize do
|
|
49
|
+
# Start the Aikido Agent only once the application starts.
|
|
50
|
+
Aikido::Zen.start!
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "bucket"
|
|
4
|
+
|
|
5
|
+
module Aikido::Zen
|
|
6
|
+
# @api private
|
|
7
|
+
#
|
|
8
|
+
# Circuit breaker that rate limits internal API requests in two ways: By using
|
|
9
|
+
# a sliding window, to allow only a certain number of events over that window,
|
|
10
|
+
# and with the ability of manually being tripped open when the API responds to
|
|
11
|
+
# a request with a 429.
|
|
12
|
+
class RateLimiter::Breaker
|
|
13
|
+
def initialize(config: Aikido::Zen.config, clock: RateLimiter::Bucket::DEFAULT_CLOCK)
|
|
14
|
+
@config = config
|
|
15
|
+
@clock = clock
|
|
16
|
+
|
|
17
|
+
@bucket = RateLimiter::Bucket.new(
|
|
18
|
+
ttl: config.client_rate_limit_period,
|
|
19
|
+
max_size: config.client_rate_limit_max_events,
|
|
20
|
+
clock: clock
|
|
21
|
+
)
|
|
22
|
+
@opened_at = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Trip the circuit open to force all events to be throttled until the
|
|
26
|
+
# deadline passes.
|
|
27
|
+
#
|
|
28
|
+
# @see Aikido::Zen::Config#server_rate_limit_deadline
|
|
29
|
+
# @return [void]
|
|
30
|
+
def open!
|
|
31
|
+
@opened_at = @clock.call
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param event_type [String] an event type which we'll use to decide
|
|
35
|
+
# if we should throttle it.
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def throttle?(event_type)
|
|
38
|
+
return true if open? && !try_close
|
|
39
|
+
|
|
40
|
+
result = @bucket.increment(event_type)
|
|
41
|
+
result.throttled?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @!visibility private
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def open?
|
|
47
|
+
@opened_at
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def past_deadline?
|
|
53
|
+
@opened_at < @clock.call - @config.server_rate_limit_deadline
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def try_close
|
|
57
|
+
@opened_at = nil if past_deadline?
|
|
58
|
+
@opened_at.nil?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../synchronizable"
|
|
4
|
+
require_relative "result"
|
|
5
|
+
|
|
6
|
+
module Aikido::Zen
|
|
7
|
+
# This models a "sliding window" rate limiting bucket (where we keep a bucket
|
|
8
|
+
# per endpoint). The timestamps of requests are kept grouped by client, and
|
|
9
|
+
# when a new request is made, we check if the number of requests falls within
|
|
10
|
+
# the configured limit.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# bucket = Aikido::Zen::RateLimiter::Bucket.new(ttl: 60, max_size: 3)
|
|
14
|
+
# bucket.increment("1.2.3.4") #=> true (count for this key: 1)
|
|
15
|
+
# bucket.increment("1.2.3.4") #=> true (count for this key: 2)
|
|
16
|
+
#
|
|
17
|
+
# # 30 seconds go by
|
|
18
|
+
# bucket.increment("1.2.3.4") #=> true (count for this key: 3)
|
|
19
|
+
#
|
|
20
|
+
# # 20 more seconds go by
|
|
21
|
+
# bucket.increment("1.2.3.4") #=> false (count for this key: 3)
|
|
22
|
+
#
|
|
23
|
+
# # 20 more seconds go by
|
|
24
|
+
# bucket.increment("1.2.3.4") #=> true (count for this key: 2)
|
|
25
|
+
#
|
|
26
|
+
class RateLimiter::Bucket
|
|
27
|
+
prepend Synchronizable
|
|
28
|
+
|
|
29
|
+
# @!visibility private
|
|
30
|
+
#
|
|
31
|
+
# Use the monotonic clock to ensure time differences are consistent
|
|
32
|
+
# and not affected by timezones.or daylight savings changes.
|
|
33
|
+
DEFAULT_CLOCK = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC).round }
|
|
34
|
+
|
|
35
|
+
def initialize(ttl:, max_size:, clock: DEFAULT_CLOCK)
|
|
36
|
+
@ttl = ttl
|
|
37
|
+
@max_size = max_size
|
|
38
|
+
@data = Hash.new { |h, k| h[k] = [] }
|
|
39
|
+
@clock = clock
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Increments the key if the number of entries within the current TTL window
|
|
43
|
+
# is below the configured threshold.
|
|
44
|
+
#
|
|
45
|
+
# @param key [String] discriminating key to identify a client.
|
|
46
|
+
# See {Aikido::Zen::Config#rate_limiting_discriminator}.
|
|
47
|
+
#
|
|
48
|
+
# @return [Aikido::Zen::RateLimiter::Result] the result of the operation and
|
|
49
|
+
# statistics on this bucket for the given key.
|
|
50
|
+
def increment(key)
|
|
51
|
+
synchronize do
|
|
52
|
+
time = @clock.call
|
|
53
|
+
evict(key, at: time)
|
|
54
|
+
|
|
55
|
+
entries = @data[key]
|
|
56
|
+
throttled = entries.size >= @max_size
|
|
57
|
+
|
|
58
|
+
entries << time unless throttled
|
|
59
|
+
|
|
60
|
+
RateLimiter::Result.new(
|
|
61
|
+
throttled: throttled,
|
|
62
|
+
discriminator: key,
|
|
63
|
+
current_requests: entries.size,
|
|
64
|
+
max_requests: @max_size,
|
|
65
|
+
time_remaining: @ttl - (time - entries.min)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def evict(key, at: @clock.call)
|
|
73
|
+
synchronize { @data[key].delete_if { |time| time < (at - @ttl) } }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Aikido::Zen
|
|
2
|
+
# Holds the stats after checking if a request should be rate limited, which
|
|
3
|
+
# will be added to the Rack env.
|
|
4
|
+
class RateLimiter::Result
|
|
5
|
+
# @return [String] the output of the configured discriminator block, used to
|
|
6
|
+
# uniquely identify a client (e.g. the remote IP).
|
|
7
|
+
attr_reader :discriminator
|
|
8
|
+
|
|
9
|
+
# @return [Integer] number of requests for the client in the current window.
|
|
10
|
+
attr_reader :current_requests
|
|
11
|
+
|
|
12
|
+
# @return [Integer] configured max number of requests per client.
|
|
13
|
+
attr_reader :max_requests
|
|
14
|
+
|
|
15
|
+
# @return [Integer] number of seconds remaining until the window resets.
|
|
16
|
+
attr_reader :time_remaining
|
|
17
|
+
|
|
18
|
+
def initialize(throttled:, discriminator:, current_requests:, max_requests:, time_remaining:)
|
|
19
|
+
@throttled = throttled
|
|
20
|
+
@discriminator = discriminator
|
|
21
|
+
@current_requests = current_requests
|
|
22
|
+
@max_requests = max_requests
|
|
23
|
+
@time_remaining = time_remaining
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Boolean] whether the current request was throttled or not.
|
|
27
|
+
def throttled?
|
|
28
|
+
@throttled
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "synchronizable"
|
|
4
|
+
require_relative "middleware/rack_throttler"
|
|
5
|
+
|
|
6
|
+
module Aikido::Zen
|
|
7
|
+
# Keeps track of all requests in this process, broken up by Route and further
|
|
8
|
+
# discriminated by client. Provides a single method that checks if a certain
|
|
9
|
+
# Request needs to be throttled or not.
|
|
10
|
+
class RateLimiter
|
|
11
|
+
prepend Synchronizable
|
|
12
|
+
|
|
13
|
+
def initialize(
|
|
14
|
+
config: Aikido::Zen.config,
|
|
15
|
+
settings: Aikido::Zen.runtime_settings
|
|
16
|
+
)
|
|
17
|
+
@config = config
|
|
18
|
+
@settings = settings
|
|
19
|
+
@buckets = Hash.new { |store, route|
|
|
20
|
+
synchronize {
|
|
21
|
+
settings = settings_for(route)
|
|
22
|
+
store[route] = Bucket.new(ttl: settings.period, max_size: settings.max_requests)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Calculate based on the configuration whether a request will be
|
|
28
|
+
# rate-limited or not.
|
|
29
|
+
#
|
|
30
|
+
# @param request [Aikido::Zen::Request]
|
|
31
|
+
# @return [Aikido::Zen::RateLimiter::Result, nil]
|
|
32
|
+
def calculate_rate_limits(request)
|
|
33
|
+
settings = settings_for(request.route)
|
|
34
|
+
return nil unless settings.enabled?
|
|
35
|
+
|
|
36
|
+
bucket = @buckets[request.route]
|
|
37
|
+
key = @config.rate_limiting_discriminator.call(request)
|
|
38
|
+
bucket.increment(key)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def settings_for(route)
|
|
44
|
+
@settings.endpoints[route].rate_limiting
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
require_relative "rate_limiter/bucket"
|
|
50
|
+
require_relative "rate_limiter/breaker"
|