aikido-zen 0.1.1-x86_64-mingw-64 → 1.0.0.pre.beta.1-x86_64-mingw-64
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 +4 -4
- data/.simplecov +7 -0
- data/CHANGELOG.md +4 -0
- data/README.md +11 -2
- data/benchmarks/README.md +8 -12
- data/benchmarks/rails7.1_sql_injection.js +30 -34
- data/docs/banner.svg +128 -129
- data/docs/config.md +8 -6
- data/docs/rails.md +1 -1
- data/lib/aikido/zen/agent.rb +13 -9
- data/lib/aikido/zen/api_client.rb +17 -7
- data/lib/aikido/zen/attack.rb +105 -36
- data/lib/aikido/zen/background_worker.rb +52 -0
- data/lib/aikido/zen/collector/routes.rb +2 -0
- data/lib/aikido/zen/collector.rb +31 -4
- data/lib/aikido/zen/config.rb +55 -20
- 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 +41 -0
- data/lib/aikido/zen/detached_agent.rb +2 -0
- data/lib/aikido/zen/errors.rb +18 -1
- data/lib/aikido/zen/event.rb +4 -2
- data/lib/aikido/zen/libzen-v0.1.37.x86_64.dll +0 -0
- data/lib/aikido/zen/middleware/check_allowed_addresses.rb +2 -14
- data/lib/aikido/zen/middleware/middleware.rb +11 -0
- data/lib/aikido/zen/middleware/{throttler.rb → rack_throttler.rb} +11 -13
- data/lib/aikido/zen/middleware/request_tracker.rb +190 -0
- data/lib/aikido/zen/middleware/set_context.rb +1 -4
- data/lib/aikido/zen/outbound_connection_monitor.rb +4 -0
- data/lib/aikido/zen/payload.rb +2 -0
- data/lib/aikido/zen/rails_engine.rb +12 -0
- data/lib/aikido/zen/rate_limiter/breaker.rb +3 -3
- data/lib/aikido/zen/rate_limiter.rb +7 -12
- data/lib/aikido/zen/request/rails_router.rb +6 -18
- data/lib/aikido/zen/request/schema/auth_schemas.rb +14 -0
- data/lib/aikido/zen/request/schema/builder.rb +0 -2
- data/lib/aikido/zen/request/schema/definition.rb +0 -5
- data/lib/aikido/zen/request/schema.rb +18 -3
- data/lib/aikido/zen/runtime_settings.rb +2 -2
- 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 +4 -6
- data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +33 -21
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +15 -7
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +6 -0
- data/lib/aikido/zen/scanners.rb +2 -0
- data/lib/aikido/zen/sink.rb +6 -1
- data/lib/aikido/zen/sinks/action_controller.rb +34 -15
- data/lib/aikido/zen/sinks/file.rb +120 -0
- data/lib/aikido/zen/sinks/kernel.rb +73 -0
- data/lib/aikido/zen/sinks/socket.rb +13 -0
- data/lib/aikido/zen/sinks.rb +8 -0
- data/lib/aikido/zen/system_info.rb +1 -1
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen/worker.rb +5 -0
- data/lib/aikido/zen.rb +54 -8
- data/tasklib/bench.rake +31 -7
- data/tasklib/wrk.rb +88 -0
- metadata +22 -8
- data/lib/aikido/zen/libzen-v0.1.31.x86_64.dll +0 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "drb/drb"
|
4
|
+
require "drb/unix"
|
5
|
+
require_relative "front_object"
|
6
|
+
require_relative "../background_worker"
|
7
|
+
|
8
|
+
module Aikido::Zen::DetachedAgent
|
9
|
+
# Agent that runs in forked processes. It communicates with the parent process to dRB
|
10
|
+
# calls. It's in charge of schedule and send heartbeats to the *parent process*, to be
|
11
|
+
# later pushed.
|
12
|
+
#
|
13
|
+
# heartbeat & polling interval are configured to 10s , because they are connecting with
|
14
|
+
# parent process. We want to have the freshest data.
|
15
|
+
#
|
16
|
+
# It's possible to use `extend Forwardable` here for one-line forward calls to the
|
17
|
+
# @detached_agent_front object. Unfortunately, the methods to be called are
|
18
|
+
# created at runtime by `DRbObject`, which leads to an ugly warning about
|
19
|
+
# private methods after the delegator is bound.
|
20
|
+
class Agent
|
21
|
+
attr_reader :worker
|
22
|
+
|
23
|
+
def initialize(
|
24
|
+
heartbeat_interval: 10,
|
25
|
+
polling_interval: 10,
|
26
|
+
config: Aikido::Zen.config,
|
27
|
+
collector: Aikido::Zen.collector,
|
28
|
+
worker: Aikido::Zen::Worker.new(config: config)
|
29
|
+
)
|
30
|
+
@config = config
|
31
|
+
@heartbeat_interval = heartbeat_interval
|
32
|
+
@polling_interval = polling_interval
|
33
|
+
@worker = worker
|
34
|
+
@collector = collector
|
35
|
+
@detached_agent_front = DRbObject.new_with_uri(config.detached_agent_socket_path)
|
36
|
+
@has_forked = false
|
37
|
+
schedule_tasks
|
38
|
+
end
|
39
|
+
|
40
|
+
def send_heartbeat(at: Time.now.utc)
|
41
|
+
return unless @collector.stats.any?
|
42
|
+
|
43
|
+
heartbeat = @collector.flush(at: at)
|
44
|
+
@detached_agent_front.send_heartbeat_to_parent_process(heartbeat.as_json)
|
45
|
+
end
|
46
|
+
|
47
|
+
private def schedule_tasks
|
48
|
+
# For heartbeats is correct to send them from parent or child process. Otherwise, we'll lose
|
49
|
+
# stats made by the parent process.
|
50
|
+
@worker.every(@heartbeat_interval, run_now: false) { send_heartbeat }
|
51
|
+
|
52
|
+
# Runtime_settings fetch must happens only in the child processes, otherwise, due to
|
53
|
+
# we are updating the global runtime_settings, we could have an infinite recursion.
|
54
|
+
if @has_forked
|
55
|
+
@worker.every(@polling_interval) do
|
56
|
+
Aikido::Zen.runtime_settings = @detached_agent_front.updated_settings
|
57
|
+
@config.logger.debug "Updated runtime settings after polling from child process #{Process.pid}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def calculate_rate_limits(request)
|
63
|
+
@detached_agent_front.calculate_rate_limits(request.route, request.ip, request.actor.to_json)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Every time a fork occurs (a new child process is created), we need to start
|
67
|
+
# a DRb service in a background thread within the child process. This service
|
68
|
+
# will manage the connection and handle resource cleanup.
|
69
|
+
def handle_fork
|
70
|
+
@has_forked = true
|
71
|
+
DRb.start_service
|
72
|
+
# we need to ensure that there are not more jobs in the queue, but
|
73
|
+
# we reuse the same object
|
74
|
+
@worker.restart
|
75
|
+
schedule_tasks
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -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,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen::DetachedAgent
|
4
|
+
class Server
|
5
|
+
def initialize(config: Aikido::Zen.config)
|
6
|
+
@detached_agent_front = FrontObject.new
|
7
|
+
@drb_server = DRb.start_service(config.detached_agent_socket_path, @detached_agent_front)
|
8
|
+
|
9
|
+
# We don't want to see drb logs unless in debug mode
|
10
|
+
@drb_server.verbose = config.logger.debug?
|
11
|
+
end
|
12
|
+
|
13
|
+
def alive?
|
14
|
+
@drb_server.alive?
|
15
|
+
end
|
16
|
+
|
17
|
+
def stop!
|
18
|
+
@drb_server.stop_service
|
19
|
+
DRb.stop_service
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
def start!
|
24
|
+
Aikido::Zen.config.logger.debug("Starting DRb Server...")
|
25
|
+
max_attempts = 10
|
26
|
+
@server = new
|
27
|
+
|
28
|
+
attempts = 0
|
29
|
+
until @server.alive?
|
30
|
+
Aikido::Zen.config.logger.info("DRb Server still not alive. #{max_attempts - attempts} attempts remaining")
|
31
|
+
sleep 0.1
|
32
|
+
attempts += 1
|
33
|
+
raise Aikido::Zen::DetachedAgentError.new("Impossible to start the dRB server (socket=#{Aikido::Zen.config.detached_agent_socket_path})") \
|
34
|
+
if attempts == max_attempts
|
35
|
+
end
|
36
|
+
|
37
|
+
@server
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/aikido/zen/errors.rb
CHANGED
@@ -60,7 +60,6 @@ module Aikido
|
|
60
60
|
attr_reader :attack
|
61
61
|
|
62
62
|
def initialize(attack)
|
63
|
-
super(attack.log_message)
|
64
63
|
@attack = attack
|
65
64
|
end
|
66
65
|
end
|
@@ -75,6 +74,16 @@ module Aikido
|
|
75
74
|
def_delegators :@attack, :request, :input
|
76
75
|
end
|
77
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
|
+
|
78
87
|
# Raised when there's any problem communicating (or loading) libzen.
|
79
88
|
class InternalsError < ZenError
|
80
89
|
# @param attempt [String] description of what we were trying to do.
|
@@ -86,5 +95,13 @@ module Aikido
|
|
86
95
|
MSG
|
87
96
|
end
|
88
97
|
end
|
98
|
+
|
99
|
+
class DetachedAgentError < ZenError
|
100
|
+
extend Forwardable
|
101
|
+
|
102
|
+
def initialize(msg)
|
103
|
+
super
|
104
|
+
end
|
105
|
+
end
|
89
106
|
end
|
90
107
|
end
|
data/lib/aikido/zen/event.rb
CHANGED
@@ -48,12 +48,13 @@ module Aikido::Zen
|
|
48
48
|
end
|
49
49
|
|
50
50
|
class Heartbeat < Event
|
51
|
-
def initialize(stats:, users:, hosts:, routes:, **opts)
|
51
|
+
def initialize(stats:, users:, hosts:, routes:, middleware_installed:, **opts)
|
52
52
|
super(type: "heartbeat", **opts)
|
53
53
|
@stats = stats
|
54
54
|
@users = users
|
55
55
|
@hosts = hosts
|
56
56
|
@routes = routes
|
57
|
+
@middleware_installed = middleware_installed
|
57
58
|
end
|
58
59
|
|
59
60
|
def as_json
|
@@ -61,7 +62,8 @@ module Aikido::Zen
|
|
61
62
|
stats: @stats.as_json,
|
62
63
|
users: @users.as_json,
|
63
64
|
routes: @routes.as_json,
|
64
|
-
hostnames: @hosts.as_json
|
65
|
+
hostnames: @hosts.as_json,
|
66
|
+
middlewareInstalled: @middleware_installed
|
65
67
|
)
|
66
68
|
end
|
67
69
|
end
|
Binary file
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "../context"
|
4
|
-
|
5
3
|
module Aikido::Zen
|
6
4
|
module Middleware
|
7
5
|
# Middleware that rejects requests from IPs blocked in the Aikido dashboard.
|
@@ -13,24 +11,14 @@ module Aikido::Zen
|
|
13
11
|
end
|
14
12
|
|
15
13
|
def call(env)
|
16
|
-
request = request_from(env)
|
14
|
+
request = Aikido::Zen::Middleware.request_from(env)
|
17
15
|
|
18
16
|
allowed_ips = @settings.endpoints[request.route].allowed_ips
|
19
17
|
|
20
18
|
if allowed_ips.empty? || allowed_ips.include?(request.ip)
|
21
19
|
@app.call(env)
|
22
20
|
else
|
23
|
-
@config.
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def request_from(env)
|
30
|
-
if (current_context = Aikido::Zen.current_context)
|
31
|
-
current_context.request
|
32
|
-
else
|
33
|
-
Context.from_rack_env(env).request
|
21
|
+
@config.blocked_responder.call(request, :ip)
|
34
22
|
end
|
35
23
|
end
|
36
24
|
end
|
@@ -4,24 +4,24 @@ require_relative "../context"
|
|
4
4
|
|
5
5
|
module Aikido::Zen
|
6
6
|
module Middleware
|
7
|
-
#
|
7
|
+
# Rack middleware that rejects requests from clients that are making too many
|
8
8
|
# requests to a given endpoint, based in the runtime configuration in the
|
9
9
|
# Aikido dashboard.
|
10
|
-
class
|
10
|
+
class RackThrottler
|
11
11
|
def initialize(
|
12
12
|
app,
|
13
13
|
config: Aikido::Zen.config,
|
14
14
|
settings: Aikido::Zen.runtime_settings,
|
15
|
-
|
15
|
+
detached_agent: Aikido::Zen.detached_agent
|
16
16
|
)
|
17
17
|
@app = app
|
18
18
|
@config = config
|
19
19
|
@settings = settings
|
20
|
-
@
|
20
|
+
@detached_agent = detached_agent
|
21
21
|
end
|
22
22
|
|
23
23
|
def call(env)
|
24
|
-
request = request_from(env)
|
24
|
+
request = Aikido::Zen::Middleware.request_from(env)
|
25
25
|
|
26
26
|
if should_throttle?(request)
|
27
27
|
@config.rate_limited_responder.call(request)
|
@@ -33,17 +33,15 @@ module Aikido::Zen
|
|
33
33
|
private
|
34
34
|
|
35
35
|
def should_throttle?(request)
|
36
|
+
return false unless @settings.endpoints[request.route].rate_limiting.enabled?
|
36
37
|
return false if @settings.skip_protection_for_ips.include?(request.ip)
|
37
38
|
|
38
|
-
@
|
39
|
-
end
|
39
|
+
result = @detached_agent.calculate_rate_limits(request)
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
Context.from_rack_env(env).request
|
46
|
-
end
|
41
|
+
return false unless result
|
42
|
+
|
43
|
+
request.env["aikido.rate_limiting"] = result
|
44
|
+
request.env["aikido.rate_limiting"].throttled?
|
47
45
|
end
|
48
46
|
end
|
49
47
|
end
|
@@ -0,0 +1,190 @@
|
|
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
|
+
Aikido::Zen.track_request request
|
17
|
+
|
18
|
+
if Aikido::Zen.config.collect_api_schema? && request.route && track?(
|
19
|
+
status_code: response[0],
|
20
|
+
route: request.route.path,
|
21
|
+
http_method: request.request_method
|
22
|
+
)
|
23
|
+
Aikido::Zen.track_discovered_route(request)
|
24
|
+
end
|
25
|
+
|
26
|
+
response
|
27
|
+
end
|
28
|
+
|
29
|
+
IGNORED_METHODS = %w[OPTIONS HEAD]
|
30
|
+
IGNORED_EXTENSIONS = %w[properties config webmanifest]
|
31
|
+
IGNORED_SEGMENTS = ["cgi-bin"]
|
32
|
+
WELL_KNOWN_URIS = %w[
|
33
|
+
/.well-known/acme-challenge
|
34
|
+
/.well-known/amphtml
|
35
|
+
/.well-known/api-catalog
|
36
|
+
/.well-known/appspecific
|
37
|
+
/.well-known/ashrae
|
38
|
+
/.well-known/assetlinks.json
|
39
|
+
/.well-known/broadband-labels
|
40
|
+
/.well-known/brski
|
41
|
+
/.well-known/caldav
|
42
|
+
/.well-known/carddav
|
43
|
+
/.well-known/change-password
|
44
|
+
/.well-known/cmp
|
45
|
+
/.well-known/coap
|
46
|
+
/.well-known/coap-eap
|
47
|
+
/.well-known/core
|
48
|
+
/.well-known/csaf
|
49
|
+
/.well-known/csaf-aggregator
|
50
|
+
/.well-known/csvm
|
51
|
+
/.well-known/did.json
|
52
|
+
/.well-known/did-configuration.json
|
53
|
+
/.well-known/dnt
|
54
|
+
/.well-known/dnt-policy.txt
|
55
|
+
/.well-known/dots
|
56
|
+
/.well-known/ecips
|
57
|
+
/.well-known/edhoc
|
58
|
+
/.well-known/enterprise-network-security
|
59
|
+
/.well-known/enterprise-transport-security
|
60
|
+
/.well-known/est
|
61
|
+
/.well-known/genid
|
62
|
+
/.well-known/gnap-as-rs
|
63
|
+
/.well-known/gpc.json
|
64
|
+
/.well-known/gs1resolver
|
65
|
+
/.well-known/hoba
|
66
|
+
/.well-known/host-meta
|
67
|
+
/.well-known/host-meta.json
|
68
|
+
/.well-known/hosting-provider
|
69
|
+
/.well-known/http-opportunistic
|
70
|
+
/.well-known/idp-proxy
|
71
|
+
/.well-known/jmap
|
72
|
+
/.well-known/keybase.txt
|
73
|
+
/.well-known/knx
|
74
|
+
/.well-known/looking-glass
|
75
|
+
/.well-known/masque
|
76
|
+
/.well-known/matrix
|
77
|
+
/.well-known/mercure
|
78
|
+
/.well-known/mta-sts.txt
|
79
|
+
/.well-known/mud
|
80
|
+
/.well-known/nfv-oauth-server-configuration
|
81
|
+
/.well-known/ni
|
82
|
+
/.well-known/nodeinfo
|
83
|
+
/.well-known/nostr.json
|
84
|
+
/.well-known/oauth-authorization-server
|
85
|
+
/.well-known/oauth-protected-resource
|
86
|
+
/.well-known/ohttp-gateway
|
87
|
+
/.well-known/openid-federation
|
88
|
+
/.well-known/open-resource-discovery
|
89
|
+
/.well-known/openid-configuration
|
90
|
+
/.well-known/openorg
|
91
|
+
/.well-known/oslc
|
92
|
+
/.well-known/pki-validation
|
93
|
+
/.well-known/posh
|
94
|
+
/.well-known/privacy-sandbox-attestations.json
|
95
|
+
/.well-known/private-token-issuer-directory
|
96
|
+
/.well-known/probing.txt
|
97
|
+
/.well-known/pvd
|
98
|
+
/.well-known/rd
|
99
|
+
/.well-known/related-website-set.json
|
100
|
+
/.well-known/reload-config
|
101
|
+
/.well-known/repute-template
|
102
|
+
/.well-known/resourcesync
|
103
|
+
/.well-known/sbom
|
104
|
+
/.well-known/security.txt
|
105
|
+
/.well-known/ssf-configuration
|
106
|
+
/.well-known/sshfp
|
107
|
+
/.well-known/stun-key
|
108
|
+
/.well-known/terraform.json
|
109
|
+
/.well-known/thread
|
110
|
+
/.well-known/time
|
111
|
+
/.well-known/timezone
|
112
|
+
/.well-known/tdmrep.json
|
113
|
+
/.well-known/tor-relay
|
114
|
+
/.well-known/tpcd
|
115
|
+
/.well-known/traffic-advice
|
116
|
+
/.well-known/trust.txt
|
117
|
+
/.well-known/uma2-configuration
|
118
|
+
/.well-known/void
|
119
|
+
/.well-known/webfinger
|
120
|
+
/.well-known/webweaver.json
|
121
|
+
/.well-known/wot
|
122
|
+
]
|
123
|
+
|
124
|
+
# @param status_code [Integer]
|
125
|
+
# @param route [String]
|
126
|
+
# @param http_method [String]
|
127
|
+
def track?(status_code:, route:, http_method:)
|
128
|
+
# In the UI we want to show only successful (2xx) or redirect (3xx) responses
|
129
|
+
# anything else is discarded.
|
130
|
+
return false unless status_code >= 200 && status_code <= 399
|
131
|
+
|
132
|
+
return false if IGNORED_METHODS.include?(http_method)
|
133
|
+
|
134
|
+
segments = route.split "/"
|
135
|
+
|
136
|
+
# Do not discover routes with dot files like `/path/to/.file` or `/.directory/file`
|
137
|
+
# We want to allow discovery of well-known URIs like `/.well-known/acme-challenge`
|
138
|
+
return false if segments.any? { |s| is_dot_file s } && !is_well_known_uri(route)
|
139
|
+
|
140
|
+
return false if segments.any? { |s| contains_ignored_string s }
|
141
|
+
|
142
|
+
# Check for every file segment if it contains a file extension and if it
|
143
|
+
# should be discovered or ignored
|
144
|
+
segments.all? { |s| should_track_extension s }
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
# Check if a path is a well-known URI
|
150
|
+
# e.g. /.well-known/acme-challenge
|
151
|
+
# https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml
|
152
|
+
def is_well_known_uri(route)
|
153
|
+
WELL_KNOWN_URIS.include?(route)
|
154
|
+
end
|
155
|
+
|
156
|
+
def is_dot_file(segment)
|
157
|
+
segment.start_with?(".") && segment.size > 1
|
158
|
+
end
|
159
|
+
|
160
|
+
def contains_ignored_string(segment)
|
161
|
+
IGNORED_SEGMENTS.any? { |ignored| segment.include?(ignored) }
|
162
|
+
end
|
163
|
+
|
164
|
+
# Ignore routes which contain file extensions
|
165
|
+
def should_track_extension(segment)
|
166
|
+
extension = get_file_extension(segment)
|
167
|
+
|
168
|
+
return true unless extension
|
169
|
+
|
170
|
+
# Do not discover files with extensions of 1 to 5 characters,
|
171
|
+
# e.g. file.css, file.js, file.woff2
|
172
|
+
return false if extension.size > 1 && extension.size < 6
|
173
|
+
|
174
|
+
# Ignore some file extensions that are longer than 5 characters or shorter than 2 chars
|
175
|
+
return false if IGNORED_EXTENSIONS.include?(extension)
|
176
|
+
|
177
|
+
true
|
178
|
+
end
|
179
|
+
|
180
|
+
def get_file_extension(segment)
|
181
|
+
extension = File.extname(segment)
|
182
|
+
if extension&.start_with?(".")
|
183
|
+
# Remove the dot from the extension
|
184
|
+
return extension[1..]
|
185
|
+
end
|
186
|
+
extension
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -15,10 +15,7 @@ module Aikido::Zen
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def call(env)
|
18
|
-
|
19
|
-
|
20
|
-
Aikido::Zen.current_context = env[ENV_KEY] = context
|
21
|
-
Aikido::Zen.track_request(context.request)
|
18
|
+
Aikido::Zen.current_context = env[ENV_KEY] = Context.from_rack_env(env)
|
22
19
|
|
23
20
|
@app.call(env)
|
24
21
|
ensure
|
@@ -5,6 +5,10 @@ module Aikido::Zen
|
|
5
5
|
# any Sink that wraps an HTTP library, and lets us keep track of any hosts to
|
6
6
|
# which the app communicates over HTTP.
|
7
7
|
module OutboundConnectionMonitor
|
8
|
+
def self.skips_on_nil_context?
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
8
12
|
# This simply reports the connection to the Agent, and always returns +nil+
|
9
13
|
# as it's not scanning for any particular attack.
|
10
14
|
#
|
data/lib/aikido/zen/payload.rb
CHANGED
@@ -14,6 +14,9 @@ module Aikido::Zen
|
|
14
14
|
|
15
15
|
app.middleware.use Aikido::Zen::Middleware::SetContext
|
16
16
|
app.middleware.use Aikido::Zen::Middleware::CheckAllowedAddresses
|
17
|
+
# Request Tracker stats do not consider failed request or 40x, so the middleware
|
18
|
+
# must be the last one wrapping the request.
|
19
|
+
app.middleware.use Aikido::Zen::Middleware::RequestTracker
|
17
20
|
|
18
21
|
ActiveSupport.on_load(:action_controller) do
|
19
22
|
# Due to how Rails sets up its middleware chain, the routing is evaluated
|
@@ -57,6 +60,15 @@ module Aikido::Zen
|
|
57
60
|
# that any gems required after aikido-zen are detected and patched
|
58
61
|
# accordingly.
|
59
62
|
Aikido::Zen.load_sinks!
|
63
|
+
|
64
|
+
# It's important we start after loading sinks, so we can report the installed packages
|
65
|
+
Aikido::Zen.start!
|
66
|
+
Aikido::Zen.start!
|
67
|
+
|
68
|
+
# Agent's bootstrap process has finished —Controllers are patched to block
|
69
|
+
# unwanted requests, sinks are loaded, scanners are running—, so we mark
|
70
|
+
# the agent as installed.
|
71
|
+
Aikido::Zen.middleware_installed!
|
60
72
|
end
|
61
73
|
end
|
62
74
|
end
|
@@ -31,13 +31,13 @@ module Aikido::Zen
|
|
31
31
|
@opened_at = @clock.call
|
32
32
|
end
|
33
33
|
|
34
|
-
# @param
|
34
|
+
# @param event_type [String] an event type which we'll use to decide
|
35
35
|
# if we should throttle it.
|
36
36
|
# @return [Boolean]
|
37
|
-
def throttle?(
|
37
|
+
def throttle?(event_type)
|
38
38
|
return true if open? && !try_close
|
39
39
|
|
40
|
-
result = @bucket.increment(
|
40
|
+
result = @bucket.increment(event_type)
|
41
41
|
result.throttled?
|
42
42
|
end
|
43
43
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "synchronizable"
|
4
|
-
require_relative "middleware/
|
4
|
+
require_relative "middleware/rack_throttler"
|
5
5
|
|
6
6
|
module Aikido::Zen
|
7
7
|
# Keeps track of all requests in this process, broken up by Route and further
|
@@ -24,23 +24,18 @@ module Aikido::Zen
|
|
24
24
|
}
|
25
25
|
end
|
26
26
|
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# the result of the check, and including useful stats in case you want to
|
30
|
-
# return RateLimit headers..
|
27
|
+
# Calculate based on the configuration whether a request will be
|
28
|
+
# rate-limited or not.
|
31
29
|
#
|
32
30
|
# @param request [Aikido::Zen::Request]
|
33
|
-
# @return [
|
34
|
-
|
35
|
-
# @see Aikido::Zen::RateLimiter::Result
|
36
|
-
def throttle?(request)
|
31
|
+
# @return [Aikido::Zen::RateLimiter::Result, nil]
|
32
|
+
def calculate_rate_limits(request)
|
37
33
|
settings = settings_for(request.route)
|
38
|
-
return
|
34
|
+
return nil unless settings.enabled?
|
39
35
|
|
40
36
|
bucket = @buckets[request.route]
|
41
37
|
key = @config.rate_limiting_discriminator.call(request)
|
42
|
-
|
43
|
-
request.env["aikido.rate_limiting"].throttled?
|
38
|
+
bucket.increment(key)
|
44
39
|
end
|
45
40
|
|
46
41
|
private
|
@@ -58,27 +58,15 @@ module Aikido::Zen
|
|
58
58
|
end
|
59
59
|
|
60
60
|
private def build_route(route, request, prefix: request.script_name)
|
61
|
-
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
module Rails
|
66
|
-
class Route < Aikido::Zen::Route
|
67
|
-
attr_reader :verb
|
61
|
+
route_wrapper = ActionDispatch::Routing::RouteWrapper.new(route)
|
68
62
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
63
|
+
path = if prefix.present?
|
64
|
+
File.join(prefix.to_s, route_wrapper.path).chomp("/")
|
65
|
+
else
|
66
|
+
route_wrapper.path
|
73
67
|
end
|
74
68
|
|
75
|
-
|
76
|
-
if @prefix.present?
|
77
|
-
File.join(@prefix.to_s, @route.path).chomp("/")
|
78
|
-
else
|
79
|
-
@route.path
|
80
|
-
end
|
81
|
-
end
|
69
|
+
Aikido::Zen::Route.new(verb: request.request_method, path: path)
|
82
70
|
end
|
83
71
|
end
|
84
72
|
end
|