aikido-zen 1.0.1.beta.2-x86_64-linux-musl
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/rails.md +70 -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 +142 -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 +279 -0
- data/lib/aikido/zen/context/rack_request.rb +24 -0
- data/lib/aikido/zen/context/rails_request.rb +42 -0
- data/lib/aikido/zen/context.rb +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 +41 -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 +102 -0
- data/lib/aikido/zen/libzen-v0.1.39-x86_64-linux-musl.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 +70 -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 +72 -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 +103 -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 +82 -0
- data/lib/aikido/zen/sinks/curb.rb +115 -0
- data/lib/aikido/zen/sinks/em_http.rb +85 -0
- data/lib/aikido/zen/sinks/excon.rb +121 -0
- data/lib/aikido/zen/sinks/file.rb +116 -0
- data/lib/aikido/zen/sinks/http.rb +95 -0
- data/lib/aikido/zen/sinks/httpclient.rb +97 -0
- data/lib/aikido/zen/sinks/httpx.rb +80 -0
- data/lib/aikido/zen/sinks/kernel.rb +34 -0
- data/lib/aikido/zen/sinks/mysql2.rb +33 -0
- data/lib/aikido/zen/sinks/net_http.rb +103 -0
- data/lib/aikido/zen/sinks/patron.rb +105 -0
- data/lib/aikido/zen/sinks/pg.rb +74 -0
- data/lib/aikido/zen/sinks/resolv.rb +62 -0
- data/lib/aikido/zen/sinks/socket.rb +80 -0
- data/lib/aikido/zen/sinks/sqlite3.rb +49 -0
- data/lib/aikido/zen/sinks/trilogy.rb +33 -0
- data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
- data/lib/aikido/zen/sinks.rb +39 -0
- data/lib/aikido/zen/sinks_dsl.rb +226 -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 +206 -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 +132 -0
- data/tasklib/wrk.rb +88 -0
- metadata +204 -0
@@ -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,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
require_relative "errors"
|
5
|
+
|
6
|
+
module Aikido::Zen
|
7
|
+
module Internals
|
8
|
+
extend FFI::Library
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# @return [String] the name of the extension we're loading, which we can
|
12
|
+
# use in error messages to identify the architecture.
|
13
|
+
attr_accessor :libzen_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.libzen_names
|
17
|
+
lib_name = "libzen-v#{LIBZEN_VERSION}"
|
18
|
+
lib_ext = FFI::Platform::LIBSUFFIX
|
19
|
+
|
20
|
+
# Gem::Platform#version should be understood as an arbitrary Ruby defined
|
21
|
+
# OS specific string. A platform with a version string is considered more
|
22
|
+
# specific than a platform without a version string.
|
23
|
+
# https://docs.ruby-lang.org/en/3.3/Gem/Platform.html
|
24
|
+
|
25
|
+
platform = Gem::Platform.local.dup
|
26
|
+
|
27
|
+
# Library names in preferred order.
|
28
|
+
#
|
29
|
+
# If two library names are added, the specific platform library names is
|
30
|
+
# first and the generic platform library name is second.
|
31
|
+
names = []
|
32
|
+
|
33
|
+
names << "#{lib_name}-#{platform}.#{lib_ext}"
|
34
|
+
|
35
|
+
unless platform.version.nil?
|
36
|
+
platform.version = nil
|
37
|
+
names << "#{lib_name}-#{platform}.#{lib_ext}"
|
38
|
+
end
|
39
|
+
|
40
|
+
names
|
41
|
+
end
|
42
|
+
|
43
|
+
# Load the most specific library
|
44
|
+
def self.load_libzen
|
45
|
+
libzen_names.each do |libzen_name|
|
46
|
+
libzen_path = File.expand_path(libzen_name, __dir__)
|
47
|
+
begin
|
48
|
+
return ffi_lib(libzen_path)
|
49
|
+
rescue LoadError
|
50
|
+
# empty
|
51
|
+
end
|
52
|
+
end
|
53
|
+
raise LoadError, "Could not load libzen"
|
54
|
+
end
|
55
|
+
|
56
|
+
begin
|
57
|
+
load_libzen
|
58
|
+
|
59
|
+
# @!method self.detect_sql_injection_native(query, input, dialect)
|
60
|
+
# @param (see .detect_sql_injection)
|
61
|
+
# @returns [Integer] 0 if no injection detected, 1 if an injection was
|
62
|
+
# detected, or 2 if there was an internal error.
|
63
|
+
# @raise [Aikido::Zen::InternalsError] if there's a problem loading or
|
64
|
+
# calling libzen.
|
65
|
+
attach_function :detect_sql_injection_native, :detect_sql_injection,
|
66
|
+
[:string, :string, :int], :int
|
67
|
+
rescue LoadError, FFI::NotFoundError => err # rubocop:disable Lint/ShadowedException
|
68
|
+
# :nocov:
|
69
|
+
|
70
|
+
# Emit an $stderr warning at startup.
|
71
|
+
warn "Zen could not load its binary extension #{libzen_name}: #{err}"
|
72
|
+
|
73
|
+
def self.detect_sql_injection(query, *)
|
74
|
+
attempt = format("%p for SQL injection", query)
|
75
|
+
raise InternalsError.new(attempt, "loading", Internals.libzen_name)
|
76
|
+
end
|
77
|
+
|
78
|
+
# :nocov:
|
79
|
+
else
|
80
|
+
# Analyzes the SQL query to detect if the provided user input is being
|
81
|
+
# passed as-is without escaping.
|
82
|
+
#
|
83
|
+
# @param query [String]
|
84
|
+
# @param input [String]
|
85
|
+
# @param dialect [Integer, #to_int] the SQL Dialect identifier in libzen.
|
86
|
+
# See {Aikido::Zen::Scanners::SQLInjectionScanner::DIALECTS}.
|
87
|
+
#
|
88
|
+
# @returns [Boolean]
|
89
|
+
# @raise [Aikido::Zen::InternalsError] if there's a problem loading or
|
90
|
+
# calling libzen.
|
91
|
+
def self.detect_sql_injection(query, input, dialect)
|
92
|
+
case detect_sql_injection_native(query, input, dialect)
|
93
|
+
when 0 then false
|
94
|
+
when 1 then true
|
95
|
+
when 2
|
96
|
+
attempt = format("%s query %p with input %p", dialect, query, input)
|
97
|
+
raise InternalsError.new(attempt, "calling detect_sql_injection in", libzen_name)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
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
|
@@ -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 SetContext
|
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,45 @@
|
|
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
|
+
# Convenience factory to create connection descriptions out of URI objects.
|
7
|
+
#
|
8
|
+
# @param uri [URI]
|
9
|
+
# @return [Aikido::Zen::OutboundConnection]
|
10
|
+
def self.from_uri(uri)
|
11
|
+
new(host: uri.hostname, port: uri.port)
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [String] the hostname or IP address to which the connection was
|
15
|
+
# attempted.
|
16
|
+
attr_reader :host
|
17
|
+
|
18
|
+
# @return [Integer] the port number to which the connection was attempted.
|
19
|
+
attr_reader :port
|
20
|
+
|
21
|
+
def initialize(host:, port:)
|
22
|
+
@host = host
|
23
|
+
@port = port
|
24
|
+
end
|
25
|
+
|
26
|
+
def as_json
|
27
|
+
{hostname: host, port: port}
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
other.is_a?(OutboundConnection) &&
|
32
|
+
host == other.host &&
|
33
|
+
port == other.port
|
34
|
+
end
|
35
|
+
alias_method :eql?, :==
|
36
|
+
|
37
|
+
def hash
|
38
|
+
[host, port].hash
|
39
|
+
end
|
40
|
+
|
41
|
+
def inspect
|
42
|
+
"#<#{self.class.name} #{host}:#{port}>"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
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
|