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,37 @@
|
|
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
|
+
collector: Aikido::Zen.collector,
|
11
|
+
runtime_settings: Aikido::Zen.runtime_settings,
|
12
|
+
rate_limiter: Aikido::Zen::RateLimiter.new
|
13
|
+
)
|
14
|
+
@config = config
|
15
|
+
@collector = collector
|
16
|
+
@rate_limiter = rate_limiter
|
17
|
+
@runtime_settings = runtime_settings
|
18
|
+
end
|
19
|
+
|
20
|
+
RequestKind = Struct.new(:route, :schema, :ip, :actor)
|
21
|
+
|
22
|
+
def send_heartbeat_to_parent_process(heartbeat)
|
23
|
+
@collector.push_heartbeat(heartbeat)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Method called by child processes to get an up-to-date version of the
|
27
|
+
# runtime_settings
|
28
|
+
def updated_settings
|
29
|
+
@runtime_settings
|
30
|
+
end
|
31
|
+
|
32
|
+
def calculate_rate_limits(route, ip, actor_hash)
|
33
|
+
actor = Aikido::Zen::Actor(actor_hash) if actor_hash
|
34
|
+
@rate_limiter.calculate_rate_limits(RequestKind.new(route, nil, ip, actor))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module Aikido::Zen::DetachedAgent
|
6
|
+
class Server
|
7
|
+
# Initialize and start a detached agent server instance.
|
8
|
+
#
|
9
|
+
# @return [Aikido::Zen::DetachedAgent::Server]
|
10
|
+
def self.start(**opts)
|
11
|
+
new(**opts).tap(&:start!)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(config: Aikido::Zen.config)
|
15
|
+
@started_at = nil
|
16
|
+
|
17
|
+
@config = config
|
18
|
+
|
19
|
+
@socket_path = config.detached_agent_socket_path
|
20
|
+
@socket_uri = config.detached_agent_socket_uri
|
21
|
+
end
|
22
|
+
|
23
|
+
def started?
|
24
|
+
!!@started_at
|
25
|
+
end
|
26
|
+
|
27
|
+
def start!
|
28
|
+
@config.logger.info("Starting DRb Server...")
|
29
|
+
|
30
|
+
# Try to ensure that the DRb service can start if the DRb service did
|
31
|
+
# not stop cleanly.
|
32
|
+
begin
|
33
|
+
# Check whether the Unix domain socket is in use by another process.
|
34
|
+
UNIXSocket.new(@socket_path).close
|
35
|
+
rescue Errno::ECONNREFUSED
|
36
|
+
@config.logger.debug("Removing residual Unix domain socket...")
|
37
|
+
|
38
|
+
# Remove the residual Unix domain socket.
|
39
|
+
FileUtils.rm_f(@socket_path)
|
40
|
+
rescue
|
41
|
+
# empty
|
42
|
+
end
|
43
|
+
|
44
|
+
@front = FrontObject.new
|
45
|
+
|
46
|
+
# If the Unix domain socket is in use by another process and/or the
|
47
|
+
# residual Unix domain socket could not be removed DRb will raise an
|
48
|
+
# appropriate error.
|
49
|
+
@drb_server = DRb.start_service(@socket_uri, @front)
|
50
|
+
|
51
|
+
# Only show DRb output in debug mode.
|
52
|
+
@drb_server.verbose = @config.logger.debug?
|
53
|
+
|
54
|
+
# Ensure that the DRb server is alive.
|
55
|
+
max_attempts = 10
|
56
|
+
attempts = 0
|
57
|
+
until @drb_server.alive?
|
58
|
+
@config.logger.info("DRb Server still not alive. #{max_attempts - attempts} attempts remaining")
|
59
|
+
sleep 0.1
|
60
|
+
attempts += 1
|
61
|
+
raise Aikido::Zen::DetachedAgentError.new("Impossible to start the dRB server (socket=#{Aikido::Zen.config.detached_agent_socket_path})") \
|
62
|
+
if attempts == max_attempts
|
63
|
+
end
|
64
|
+
|
65
|
+
@started_at = Time.now.utc
|
66
|
+
|
67
|
+
at_exit { stop! if started? }
|
68
|
+
end
|
69
|
+
|
70
|
+
def stop!
|
71
|
+
@config.logger.info("Stopping DRb Server...")
|
72
|
+
@started_at = nil
|
73
|
+
|
74
|
+
@drb_server.stop_service if @drb_server.alive?
|
75
|
+
DRb.stop_service
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Aikido
|
6
|
+
# @!visibility private
|
7
|
+
# Support rescuing Aikido::Error without forcing a single base class to all
|
8
|
+
# errors (so things that should be e.g. a TypeError, can have the correct
|
9
|
+
# superclass).
|
10
|
+
module Error; end # :nodoc:
|
11
|
+
|
12
|
+
# @!visibility private
|
13
|
+
# Generic error for problems with the Agent.
|
14
|
+
class ZenError < RuntimeError
|
15
|
+
include Error
|
16
|
+
end
|
17
|
+
|
18
|
+
module Zen
|
19
|
+
# Wrapper for all low-level network errors communicating with the API. You
|
20
|
+
# can access the original error by calling #cause.
|
21
|
+
class NetworkError < StandardError
|
22
|
+
include Error
|
23
|
+
|
24
|
+
def initialize(request, cause = nil)
|
25
|
+
@request = request.dup
|
26
|
+
|
27
|
+
super("Error in #{request.method} #{request.path}: #{cause.message}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Raised whenever a request to the API results in a 4XX or 5XX response.
|
32
|
+
class APIError < StandardError
|
33
|
+
include Error
|
34
|
+
|
35
|
+
attr_reader :request
|
36
|
+
attr_reader :response
|
37
|
+
|
38
|
+
def initialize(request, response)
|
39
|
+
@request = anonimize_token(request.dup)
|
40
|
+
@response = response
|
41
|
+
|
42
|
+
super("Error in #{request.method} #{request.path}: #{response.code} #{response.message} (#{response.body})")
|
43
|
+
end
|
44
|
+
|
45
|
+
private def anonimize_token(request)
|
46
|
+
# Anonimize the token to `********************xxxx`,
|
47
|
+
# mimicking what we show in the dashbaord.
|
48
|
+
request["Authorization"] = request["Authorization"].to_s
|
49
|
+
.gsub(/\A.*(.{4})\z/, ("*" * 20) + "\\1")
|
50
|
+
request
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Raised whenever a response to the API results in a 429 response.
|
55
|
+
class RateLimitedError < APIError; end
|
56
|
+
|
57
|
+
class UnderAttackError < StandardError
|
58
|
+
include Error
|
59
|
+
|
60
|
+
attr_reader :attack
|
61
|
+
|
62
|
+
def initialize(attack)
|
63
|
+
@attack = attack
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class SQLInjectionError < UnderAttackError
|
68
|
+
extend Forwardable
|
69
|
+
def_delegators :@attack, :query, :input, :dialect
|
70
|
+
end
|
71
|
+
|
72
|
+
class SSRFDetectedError < UnderAttackError
|
73
|
+
extend Forwardable
|
74
|
+
def_delegators :@attack, :request, :input
|
75
|
+
end
|
76
|
+
|
77
|
+
class PathTraversalError < UnderAttackError
|
78
|
+
extend Forwardable
|
79
|
+
def_delegators :@attack, :input
|
80
|
+
end
|
81
|
+
|
82
|
+
class ShellInjectionError < UnderAttackError
|
83
|
+
extend Forwardable
|
84
|
+
def_delegators :@attack, :input
|
85
|
+
end
|
86
|
+
|
87
|
+
# Raised when there's any problem communicating (or loading) libzen.
|
88
|
+
class InternalsError < ZenError
|
89
|
+
# @param attempt [String] description of what we were trying to do.
|
90
|
+
# @param problem [String] what couldn't be done.
|
91
|
+
# @param libname [String] the name of the file (including the arch).
|
92
|
+
def initialize(attempt, problem, libname)
|
93
|
+
super(format(<<~MSG.chomp, attempt, problem, libname))
|
94
|
+
Zen could not scan %s due to a problem %s the library `%s'
|
95
|
+
MSG
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class DetachedAgentError < ZenError
|
100
|
+
extend Forwardable
|
101
|
+
|
102
|
+
def initialize(msg)
|
103
|
+
super
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,71 @@
|
|
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:, users:, hosts:, routes:, middleware_installed:, **opts)
|
52
|
+
super(type: "heartbeat", **opts)
|
53
|
+
@stats = stats
|
54
|
+
@users = users
|
55
|
+
@hosts = hosts
|
56
|
+
@routes = routes
|
57
|
+
@middleware_installed = middleware_installed
|
58
|
+
end
|
59
|
+
|
60
|
+
def as_json
|
61
|
+
super.update(
|
62
|
+
stats: @stats.as_json,
|
63
|
+
users: @users.as_json,
|
64
|
+
routes: @routes.as_json,
|
65
|
+
hostnames: @hosts.as_json,
|
66
|
+
middlewareInstalled: @middleware_installed
|
67
|
+
)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,103 @@
|
|
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
|
+
def self.libzen_names
|
11
|
+
lib_name = "libzen-v#{LIBZEN_VERSION}"
|
12
|
+
lib_ext = FFI::Platform::LIBSUFFIX
|
13
|
+
|
14
|
+
# Gem::Platform#version should be understood as an arbitrary Ruby defined
|
15
|
+
# OS specific string. A platform with a version string is considered more
|
16
|
+
# specific than a platform without a version string.
|
17
|
+
# https://docs.ruby-lang.org/en/3.3/Gem/Platform.html
|
18
|
+
|
19
|
+
platform = Gem::Platform.local.dup
|
20
|
+
|
21
|
+
# Library names in preferred order.
|
22
|
+
#
|
23
|
+
# If two library names are added, the specific platform library names is
|
24
|
+
# first and the generic platform library name is second.
|
25
|
+
names = []
|
26
|
+
|
27
|
+
names << "#{lib_name}-#{platform}.#{lib_ext}"
|
28
|
+
|
29
|
+
unless platform.version.nil?
|
30
|
+
platform.version = nil
|
31
|
+
names << "#{lib_name}-#{platform}.#{lib_ext}"
|
32
|
+
end
|
33
|
+
|
34
|
+
names
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [String] the name of the extension we're loading, which we can
|
38
|
+
# use in error messages.
|
39
|
+
def self.libzen_name
|
40
|
+
# The most generic platform library name.
|
41
|
+
libzen_names.last
|
42
|
+
end
|
43
|
+
|
44
|
+
# Load the most specific library
|
45
|
+
def self.load_libzen
|
46
|
+
libzen_names.each do |name|
|
47
|
+
path = File.expand_path(name, __dir__)
|
48
|
+
begin
|
49
|
+
return ffi_lib(path)
|
50
|
+
rescue LoadError
|
51
|
+
# empty
|
52
|
+
end
|
53
|
+
end
|
54
|
+
raise LoadError, "Zen could not load its native extension #{libzen_name}"
|
55
|
+
end
|
56
|
+
|
57
|
+
begin
|
58
|
+
load_libzen
|
59
|
+
|
60
|
+
# @!method self.detect_sql_injection_native(query, input, dialect)
|
61
|
+
# @param (see .detect_sql_injection)
|
62
|
+
# @returns [Integer] 0 if no injection detected, 1 if an injection was
|
63
|
+
# detected, or 2 if there was an internal error.
|
64
|
+
# @raise [Aikido::Zen::InternalsError] if there's a problem loading or
|
65
|
+
# calling libzen.
|
66
|
+
attach_function :detect_sql_injection_native, :detect_sql_injection,
|
67
|
+
[:string, :string, :int], :int
|
68
|
+
rescue LoadError, FFI::NotFoundError => err # rubocop:disable Lint/ShadowedException
|
69
|
+
# :nocov:
|
70
|
+
|
71
|
+
# Emit an $stderr warning at startup.
|
72
|
+
warn "Zen could not load its native extension #{libzen_name}: #{err}"
|
73
|
+
|
74
|
+
def self.detect_sql_injection(query, *)
|
75
|
+
attempt = format("%p for SQL injection", query)
|
76
|
+
raise InternalsError.new(attempt, "loading", libzen_name)
|
77
|
+
end
|
78
|
+
|
79
|
+
# :nocov:
|
80
|
+
else
|
81
|
+
# Analyzes the SQL query to detect if the provided user input is being
|
82
|
+
# passed as-is without escaping.
|
83
|
+
#
|
84
|
+
# @param query [String]
|
85
|
+
# @param input [String]
|
86
|
+
# @param dialect [Integer, #to_int] the SQL Dialect identifier in libzen.
|
87
|
+
# See {Aikido::Zen::Scanners::SQLInjectionScanner::DIALECTS}.
|
88
|
+
#
|
89
|
+
# @returns [Boolean]
|
90
|
+
# @raise [Aikido::Zen::InternalsError] if there's a problem loading or
|
91
|
+
# calling libzen.
|
92
|
+
def self.detect_sql_injection(query, input, dialect)
|
93
|
+
case detect_sql_injection_native(query, input, dialect)
|
94
|
+
when 0 then false
|
95
|
+
when 1 then true
|
96
|
+
when 2
|
97
|
+
attempt = format("%s query %p with input %p", dialect, query, input)
|
98
|
+
raise InternalsError.new(attempt, "calling detect_sql_injection in", libzen_name)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
Binary file
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen
|
4
|
+
module Middleware
|
5
|
+
# Middleware that rejects requests from IPs blocked in the Aikido dashboard.
|
6
|
+
class CheckAllowedAddresses
|
7
|
+
def initialize(app, config: Aikido::Zen.config, settings: Aikido::Zen.runtime_settings)
|
8
|
+
@app = app
|
9
|
+
@config = config
|
10
|
+
@settings = settings
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
request = Aikido::Zen::Middleware.request_from(env)
|
15
|
+
|
16
|
+
allowed_ips = @settings.endpoints[request.route].allowed_ips
|
17
|
+
|
18
|
+
if allowed_ips.empty? || allowed_ips.include?(request.ip)
|
19
|
+
@app.call(env)
|
20
|
+
else
|
21
|
+
@config.blocked_responder.call(request, :ip)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../context"
|
4
|
+
|
5
|
+
module Aikido::Zen
|
6
|
+
module Middleware
|
7
|
+
# Rack middleware that rejects requests from clients that are making too many
|
8
|
+
# requests to a given endpoint, based in the runtime configuration in the
|
9
|
+
# Aikido dashboard.
|
10
|
+
class RackThrottler
|
11
|
+
def initialize(
|
12
|
+
app,
|
13
|
+
config: Aikido::Zen.config,
|
14
|
+
settings: Aikido::Zen.runtime_settings,
|
15
|
+
detached_agent: Aikido::Zen.detached_agent
|
16
|
+
)
|
17
|
+
@app = app
|
18
|
+
@config = config
|
19
|
+
@settings = settings
|
20
|
+
@detached_agent = detached_agent
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(env)
|
24
|
+
request = Aikido::Zen::Middleware.request_from(env)
|
25
|
+
|
26
|
+
if should_throttle?(request)
|
27
|
+
@config.rate_limited_responder.call(request)
|
28
|
+
else
|
29
|
+
@app.call(env)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def should_throttle?(request)
|
36
|
+
return false unless @settings.endpoints[request.route].rate_limiting.enabled?
|
37
|
+
return false if @settings.skip_protection_for_ips.include?(request.ip)
|
38
|
+
|
39
|
+
result = @detached_agent.calculate_rate_limits(request)
|
40
|
+
|
41
|
+
return false unless result
|
42
|
+
|
43
|
+
request.env["aikido.rate_limiting"] = result
|
44
|
+
request.env["aikido.rate_limiting"].throttled?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,192 @@
|
|
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)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
request = Aikido::Zen::Middleware.request_from(env)
|
14
|
+
response = @app.call(env)
|
15
|
+
|
16
|
+
if request.route && track?(
|
17
|
+
status_code: response[0],
|
18
|
+
route: request.route.path,
|
19
|
+
http_method: request.request_method
|
20
|
+
)
|
21
|
+
Aikido::Zen.track_request request
|
22
|
+
|
23
|
+
if Aikido::Zen.config.collect_api_schema?
|
24
|
+
Aikido::Zen.track_discovered_route(request)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
response
|
29
|
+
end
|
30
|
+
|
31
|
+
IGNORED_METHODS = %w[OPTIONS HEAD]
|
32
|
+
IGNORED_EXTENSIONS = %w[properties config webmanifest]
|
33
|
+
IGNORED_SEGMENTS = ["cgi-bin"]
|
34
|
+
WELL_KNOWN_URIS = %w[
|
35
|
+
/.well-known/acme-challenge
|
36
|
+
/.well-known/amphtml
|
37
|
+
/.well-known/api-catalog
|
38
|
+
/.well-known/appspecific
|
39
|
+
/.well-known/ashrae
|
40
|
+
/.well-known/assetlinks.json
|
41
|
+
/.well-known/broadband-labels
|
42
|
+
/.well-known/brski
|
43
|
+
/.well-known/caldav
|
44
|
+
/.well-known/carddav
|
45
|
+
/.well-known/change-password
|
46
|
+
/.well-known/cmp
|
47
|
+
/.well-known/coap
|
48
|
+
/.well-known/coap-eap
|
49
|
+
/.well-known/core
|
50
|
+
/.well-known/csaf
|
51
|
+
/.well-known/csaf-aggregator
|
52
|
+
/.well-known/csvm
|
53
|
+
/.well-known/did.json
|
54
|
+
/.well-known/did-configuration.json
|
55
|
+
/.well-known/dnt
|
56
|
+
/.well-known/dnt-policy.txt
|
57
|
+
/.well-known/dots
|
58
|
+
/.well-known/ecips
|
59
|
+
/.well-known/edhoc
|
60
|
+
/.well-known/enterprise-network-security
|
61
|
+
/.well-known/enterprise-transport-security
|
62
|
+
/.well-known/est
|
63
|
+
/.well-known/genid
|
64
|
+
/.well-known/gnap-as-rs
|
65
|
+
/.well-known/gpc.json
|
66
|
+
/.well-known/gs1resolver
|
67
|
+
/.well-known/hoba
|
68
|
+
/.well-known/host-meta
|
69
|
+
/.well-known/host-meta.json
|
70
|
+
/.well-known/hosting-provider
|
71
|
+
/.well-known/http-opportunistic
|
72
|
+
/.well-known/idp-proxy
|
73
|
+
/.well-known/jmap
|
74
|
+
/.well-known/keybase.txt
|
75
|
+
/.well-known/knx
|
76
|
+
/.well-known/looking-glass
|
77
|
+
/.well-known/masque
|
78
|
+
/.well-known/matrix
|
79
|
+
/.well-known/mercure
|
80
|
+
/.well-known/mta-sts.txt
|
81
|
+
/.well-known/mud
|
82
|
+
/.well-known/nfv-oauth-server-configuration
|
83
|
+
/.well-known/ni
|
84
|
+
/.well-known/nodeinfo
|
85
|
+
/.well-known/nostr.json
|
86
|
+
/.well-known/oauth-authorization-server
|
87
|
+
/.well-known/oauth-protected-resource
|
88
|
+
/.well-known/ohttp-gateway
|
89
|
+
/.well-known/openid-federation
|
90
|
+
/.well-known/open-resource-discovery
|
91
|
+
/.well-known/openid-configuration
|
92
|
+
/.well-known/openorg
|
93
|
+
/.well-known/oslc
|
94
|
+
/.well-known/pki-validation
|
95
|
+
/.well-known/posh
|
96
|
+
/.well-known/privacy-sandbox-attestations.json
|
97
|
+
/.well-known/private-token-issuer-directory
|
98
|
+
/.well-known/probing.txt
|
99
|
+
/.well-known/pvd
|
100
|
+
/.well-known/rd
|
101
|
+
/.well-known/related-website-set.json
|
102
|
+
/.well-known/reload-config
|
103
|
+
/.well-known/repute-template
|
104
|
+
/.well-known/resourcesync
|
105
|
+
/.well-known/sbom
|
106
|
+
/.well-known/security.txt
|
107
|
+
/.well-known/ssf-configuration
|
108
|
+
/.well-known/sshfp
|
109
|
+
/.well-known/stun-key
|
110
|
+
/.well-known/terraform.json
|
111
|
+
/.well-known/thread
|
112
|
+
/.well-known/time
|
113
|
+
/.well-known/timezone
|
114
|
+
/.well-known/tdmrep.json
|
115
|
+
/.well-known/tor-relay
|
116
|
+
/.well-known/tpcd
|
117
|
+
/.well-known/traffic-advice
|
118
|
+
/.well-known/trust.txt
|
119
|
+
/.well-known/uma2-configuration
|
120
|
+
/.well-known/void
|
121
|
+
/.well-known/webfinger
|
122
|
+
/.well-known/webweaver.json
|
123
|
+
/.well-known/wot
|
124
|
+
]
|
125
|
+
|
126
|
+
# @param status_code [Integer]
|
127
|
+
# @param route [String]
|
128
|
+
# @param http_method [String]
|
129
|
+
def track?(status_code:, route:, http_method:)
|
130
|
+
# In the UI we want to show only successful (2xx) or redirect (3xx) responses
|
131
|
+
# anything else is discarded.
|
132
|
+
return false unless status_code >= 200 && status_code <= 399
|
133
|
+
|
134
|
+
return false if IGNORED_METHODS.include?(http_method)
|
135
|
+
|
136
|
+
segments = route.split "/"
|
137
|
+
|
138
|
+
# Do not discover routes with dot files like `/path/to/.file` or `/.directory/file`
|
139
|
+
# We want to allow discovery of well-known URIs like `/.well-known/acme-challenge`
|
140
|
+
return false if segments.any? { |s| is_dot_file s } && !is_well_known_uri(route)
|
141
|
+
|
142
|
+
return false if segments.any? { |s| contains_ignored_string s }
|
143
|
+
|
144
|
+
# Check for every file segment if it contains a file extension and if it
|
145
|
+
# should be discovered or ignored
|
146
|
+
segments.all? { |s| should_track_extension s }
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
# Check if a path is a well-known URI
|
152
|
+
# e.g. /.well-known/acme-challenge
|
153
|
+
# https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml
|
154
|
+
def is_well_known_uri(route)
|
155
|
+
WELL_KNOWN_URIS.include?(route)
|
156
|
+
end
|
157
|
+
|
158
|
+
def is_dot_file(segment)
|
159
|
+
segment.start_with?(".") && segment.size > 1
|
160
|
+
end
|
161
|
+
|
162
|
+
def contains_ignored_string(segment)
|
163
|
+
IGNORED_SEGMENTS.any? { |ignored| segment.include?(ignored) }
|
164
|
+
end
|
165
|
+
|
166
|
+
# Ignore routes which contain file extensions
|
167
|
+
def should_track_extension(segment)
|
168
|
+
extension = get_file_extension(segment)
|
169
|
+
|
170
|
+
return true unless extension
|
171
|
+
|
172
|
+
# Do not discover files with extensions of 1 to 5 characters,
|
173
|
+
# e.g. file.css, file.js, file.woff2
|
174
|
+
return false if extension.size > 1 && extension.size < 6
|
175
|
+
|
176
|
+
# Ignore some file extensions that are longer than 5 characters or shorter than 2 chars
|
177
|
+
return false if IGNORED_EXTENSIONS.include?(extension)
|
178
|
+
|
179
|
+
true
|
180
|
+
end
|
181
|
+
|
182
|
+
def get_file_extension(segment)
|
183
|
+
extension = File.extname(segment)
|
184
|
+
if extension&.start_with?(".")
|
185
|
+
# Remove the dot from the extension
|
186
|
+
return extension[1..]
|
187
|
+
end
|
188
|
+
extension
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|