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,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,116 @@
|
|
|
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
|
+
{
|
|
45
|
+
attack: @attack.as_json,
|
|
46
|
+
request: @attack.context&.request&.as_json
|
|
47
|
+
}.compact
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class Heartbeat < Event
|
|
53
|
+
def initialize(stats:, users:, hosts:, routes:, middleware_installed:, **opts)
|
|
54
|
+
super(type: "heartbeat", **opts)
|
|
55
|
+
@stats = stats
|
|
56
|
+
@users = users
|
|
57
|
+
@hosts = hosts
|
|
58
|
+
@routes = routes
|
|
59
|
+
@middleware_installed = middleware_installed
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def as_json
|
|
63
|
+
super.update(
|
|
64
|
+
stats: @stats.as_json,
|
|
65
|
+
users: @users.as_json,
|
|
66
|
+
routes: @routes.as_json,
|
|
67
|
+
hostnames: @hosts.as_json,
|
|
68
|
+
middlewareInstalled: @middleware_installed
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class AttackWave < Event
|
|
74
|
+
# @param [Aikido::Zen::Context] a context
|
|
75
|
+
# @return [Aikido::Zen::Events::AttackWave] an attack wave event
|
|
76
|
+
def self.from_context(context)
|
|
77
|
+
request = Aikido::Zen::AttackWave::Request.new(
|
|
78
|
+
ip_address: context.request.client_ip,
|
|
79
|
+
user_agent: context.request.user_agent,
|
|
80
|
+
source: context.request.framework
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
attack = Aikido::Zen::AttackWave::Attack.new(
|
|
84
|
+
metadata: {}, # not used yet
|
|
85
|
+
user: context.request.actor
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
new(request: request, attack: attack)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [Aikido::Zen::AttackWave::Request]
|
|
92
|
+
attr_reader :request
|
|
93
|
+
|
|
94
|
+
# @return [Aikido::Zen::AttackWave::Attack]
|
|
95
|
+
attr_reader :attack
|
|
96
|
+
|
|
97
|
+
# @param [Aikido::Zen::AttackWave::Request] the attack wave request
|
|
98
|
+
# @param [Aikido::Zen::AttackWave::Attack] the attack wave attack
|
|
99
|
+
# @param opts [Hash<Symbol, Object>] any other options to pass to
|
|
100
|
+
# the superclass initializer.
|
|
101
|
+
# @return [Aikido::Zen::Events::AttackWave] an attack wave event
|
|
102
|
+
def initialize(request:, attack:, **opts)
|
|
103
|
+
super(type: "detected_attack_wave", **opts)
|
|
104
|
+
@request = request
|
|
105
|
+
@attack = attack
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def as_json
|
|
109
|
+
super.update(
|
|
110
|
+
request: @request.as_json,
|
|
111
|
+
attack: @attack.as_json
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido
|
|
4
|
+
module Zen
|
|
5
|
+
# @api private
|
|
6
|
+
module Helpers
|
|
7
|
+
# Normalizes a path by:
|
|
8
|
+
#
|
|
9
|
+
# 1. Collapsing consecutive forward slashes into a single forward slash.
|
|
10
|
+
# 2. Removing forward trailing slash, unless the normalized path is "/".
|
|
11
|
+
#
|
|
12
|
+
# @param path [String, nil] the path to normalize.
|
|
13
|
+
# @return [String, nil] the normalized path.
|
|
14
|
+
def self.normalize_path(path)
|
|
15
|
+
return path unless path
|
|
16
|
+
|
|
17
|
+
normalized_path = path.dup
|
|
18
|
+
normalized_path.squeeze!("/")
|
|
19
|
+
normalized_path.chomp!("/") unless normalized_path == "/"
|
|
20
|
+
normalized_path
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
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, 2 if there was an internal error, or 3 if SQL tokenization failed.
|
|
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
|
+
[:pointer, :size_t, :pointer, :size_t, :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
|
+
query_bytes = encode_safely(query)
|
|
94
|
+
input_bytes = encode_safely(input)
|
|
95
|
+
|
|
96
|
+
query_ptr = FFI::MemoryPointer.new(:uint8, query_bytes.bytesize)
|
|
97
|
+
input_ptr = FFI::MemoryPointer.new(:uint8, input_bytes.bytesize)
|
|
98
|
+
|
|
99
|
+
query_ptr.put_bytes(0, query_bytes)
|
|
100
|
+
input_ptr.put_bytes(0, input_bytes)
|
|
101
|
+
|
|
102
|
+
case detect_sql_injection_native(query_ptr, query_bytes.bytesize, input_ptr, input_bytes.bytesize, dialect)
|
|
103
|
+
when 0 then false
|
|
104
|
+
when 1 then true
|
|
105
|
+
when 2
|
|
106
|
+
attempt = format("%s query %p with input %p", dialect, query, input)
|
|
107
|
+
raise InternalsError.new(attempt, "calling detect_sql_injection in", libzen_name)
|
|
108
|
+
when 3
|
|
109
|
+
# SQL tokenization failed - return false (no injection detected)
|
|
110
|
+
false
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class << self
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def encode_safely(string)
|
|
119
|
+
string.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
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 AllowedAddressChecker
|
|
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,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido
|
|
4
|
+
module Zen
|
|
5
|
+
module Middleware
|
|
6
|
+
class AttackWaveProtector
|
|
7
|
+
def initialize(app, zen: Aikido::Zen, settings: Aikido::Zen.runtime_settings)
|
|
8
|
+
@app = app
|
|
9
|
+
@zen = zen
|
|
10
|
+
@settings = settings
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
response = @app.call(env)
|
|
15
|
+
|
|
16
|
+
context = @zen.current_context
|
|
17
|
+
protect(context)
|
|
18
|
+
|
|
19
|
+
response
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @api private
|
|
23
|
+
# Visible for testing.
|
|
24
|
+
def attack_wave?(context)
|
|
25
|
+
request = context.request
|
|
26
|
+
return false if request.nil?
|
|
27
|
+
|
|
28
|
+
# Bypass attack wave protection for allowed IPs
|
|
29
|
+
return false if @settings.allowed_ips.include?(request.ip)
|
|
30
|
+
|
|
31
|
+
@zen.attack_wave_detector.attack_wave?(context)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @api private
|
|
35
|
+
# Visible for testing.
|
|
36
|
+
def protect(context)
|
|
37
|
+
if attack_wave?(context)
|
|
38
|
+
attack_wave = Aikido::Zen::Events::AttackWave.from_context(context)
|
|
39
|
+
@zen.track_attack_wave(attack_wave)
|
|
40
|
+
@zen.agent.report(attack_wave)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../context"
|
|
4
|
+
|
|
5
|
+
module Aikido::Zen
|
|
6
|
+
# @!visibility private
|
|
7
|
+
ENV_KEY = "aikido.context"
|
|
8
|
+
|
|
9
|
+
module Middleware
|
|
10
|
+
# Rack middleware that keeps the current context in a Thread/Fiber-local
|
|
11
|
+
# variable so that other parts of the agent/firewall can access it.
|
|
12
|
+
class ContextSetter
|
|
13
|
+
def initialize(app)
|
|
14
|
+
@app = app
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(env)
|
|
18
|
+
Aikido::Zen.current_context = env[ENV_KEY] = Context.from_rack_env(env)
|
|
19
|
+
|
|
20
|
+
@app.call(env)
|
|
21
|
+
ensure
|
|
22
|
+
Aikido::Zen.current_context = nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido
|
|
4
|
+
module Zen
|
|
5
|
+
module Middleware
|
|
6
|
+
# This middleware is responsible for detecting when a process has forked
|
|
7
|
+
# (e.g., in a Puma or Unicorn worker) and resetting the state of the
|
|
8
|
+
# Aikido Zen agent. It should be inserted early in the middleware stack.
|
|
9
|
+
class ForkDetector
|
|
10
|
+
def initialize(app)
|
|
11
|
+
@app = app
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
# This is the single, reliable trigger point for the fork check.
|
|
16
|
+
Aikido::Zen.check_and_handle_fork
|
|
17
|
+
|
|
18
|
+
@app.call(env)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
# Bypass rate limiting for allowed IPs
|
|
37
|
+
return false if @settings.allowed_ips.include?(request.ip)
|
|
38
|
+
|
|
39
|
+
return false unless @settings.endpoints[request.route].rate_limiting.enabled?
|
|
40
|
+
|
|
41
|
+
result = @detached_agent.calculate_rate_limits(request)
|
|
42
|
+
|
|
43
|
+
return false unless result
|
|
44
|
+
|
|
45
|
+
request.env["aikido.rate_limiting"] = result
|
|
46
|
+
request.env["aikido.rate_limiting"].throttled?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|