aikido-zen 0.1.0.alpha4-arm64-darwin

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.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/.standard.yml +3 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE +674 -0
  7. data/README.md +40 -0
  8. data/Rakefile +63 -0
  9. data/lib/aikido/zen/actor.rb +116 -0
  10. data/lib/aikido/zen/agent.rb +187 -0
  11. data/lib/aikido/zen/api_client.rb +132 -0
  12. data/lib/aikido/zen/attack.rb +138 -0
  13. data/lib/aikido/zen/capped_collections.rb +68 -0
  14. data/lib/aikido/zen/config.rb +229 -0
  15. data/lib/aikido/zen/context/rack_request.rb +24 -0
  16. data/lib/aikido/zen/context/rails_request.rb +42 -0
  17. data/lib/aikido/zen/context.rb +101 -0
  18. data/lib/aikido/zen/errors.rb +88 -0
  19. data/lib/aikido/zen/event.rb +66 -0
  20. data/lib/aikido/zen/internals.rb +64 -0
  21. data/lib/aikido/zen/libzen-v0.1.26.aarch64.dylib +0 -0
  22. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +38 -0
  23. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  24. data/lib/aikido/zen/middleware/throttler.rb +50 -0
  25. data/lib/aikido/zen/outbound_connection.rb +45 -0
  26. data/lib/aikido/zen/outbound_connection_monitor.rb +19 -0
  27. data/lib/aikido/zen/package.rb +22 -0
  28. data/lib/aikido/zen/payload.rb +48 -0
  29. data/lib/aikido/zen/rails_engine.rb +53 -0
  30. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  31. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  32. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  33. data/lib/aikido/zen/rate_limiter.rb +55 -0
  34. data/lib/aikido/zen/request/heuristic_router.rb +109 -0
  35. data/lib/aikido/zen/request/rails_router.rb +84 -0
  36. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  37. data/lib/aikido/zen/request/schema/auth_schemas.rb +40 -0
  38. data/lib/aikido/zen/request/schema/builder.rb +125 -0
  39. data/lib/aikido/zen/request/schema/definition.rb +112 -0
  40. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  41. data/lib/aikido/zen/request/schema.rb +72 -0
  42. data/lib/aikido/zen/request.rb +97 -0
  43. data/lib/aikido/zen/route.rb +39 -0
  44. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  45. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  46. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  47. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  48. data/lib/aikido/zen/runtime_settings.rb +70 -0
  49. data/lib/aikido/zen/scan.rb +75 -0
  50. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +95 -0
  51. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  52. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +85 -0
  53. data/lib/aikido/zen/scanners/ssrf_scanner.rb +251 -0
  54. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +43 -0
  55. data/lib/aikido/zen/scanners.rb +5 -0
  56. data/lib/aikido/zen/sink.rb +108 -0
  57. data/lib/aikido/zen/sinks/async_http.rb +63 -0
  58. data/lib/aikido/zen/sinks/curb.rb +89 -0
  59. data/lib/aikido/zen/sinks/em_http.rb +71 -0
  60. data/lib/aikido/zen/sinks/excon.rb +103 -0
  61. data/lib/aikido/zen/sinks/http.rb +76 -0
  62. data/lib/aikido/zen/sinks/httpclient.rb +68 -0
  63. data/lib/aikido/zen/sinks/httpx.rb +61 -0
  64. data/lib/aikido/zen/sinks/mysql2.rb +21 -0
  65. data/lib/aikido/zen/sinks/net_http.rb +85 -0
  66. data/lib/aikido/zen/sinks/patron.rb +88 -0
  67. data/lib/aikido/zen/sinks/pg.rb +50 -0
  68. data/lib/aikido/zen/sinks/resolv.rb +41 -0
  69. data/lib/aikido/zen/sinks/socket.rb +51 -0
  70. data/lib/aikido/zen/sinks/sqlite3.rb +30 -0
  71. data/lib/aikido/zen/sinks/trilogy.rb +21 -0
  72. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  73. data/lib/aikido/zen/sinks.rb +21 -0
  74. data/lib/aikido/zen/stats/routes.rb +53 -0
  75. data/lib/aikido/zen/stats/sink_stats.rb +95 -0
  76. data/lib/aikido/zen/stats/users.rb +26 -0
  77. data/lib/aikido/zen/stats.rb +171 -0
  78. data/lib/aikido/zen/synchronizable.rb +24 -0
  79. data/lib/aikido/zen/system_info.rb +84 -0
  80. data/lib/aikido/zen/version.rb +10 -0
  81. data/lib/aikido/zen.rb +138 -0
  82. data/lib/aikido-zen.rb +3 -0
  83. data/lib/aikido.rb +3 -0
  84. data/tasklib/libzen.rake +128 -0
  85. metadata +175 -0
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../route"
4
+ require_relative "protection_settings"
5
+
6
+ module Aikido::Zen
7
+ # Wraps the list of endpoint protection settings, providing an interface for
8
+ # checking the settings for any given route. If the route has no configured
9
+ # settings, that will return the singleton
10
+ # {RuntimeSettings::ProtectionSettings.none}.
11
+ #
12
+ # @example
13
+ # endpoint = runtime_settings.endpoints[request.route]
14
+ # block_request unless endpoint.allows?(request.ip)
15
+ class RuntimeSettings::Endpoints
16
+ # @param data [Array<Hash>]
17
+ # @return [Aikido::Zen::RuntimeSettings::Endpoints]
18
+ def self.from_json(data)
19
+ data = Array(data).map { |item|
20
+ route = Route.new(verb: item["method"], path: item["route"])
21
+ settings = RuntimeSettings::ProtectionSettings.from_json(item)
22
+ [route, settings]
23
+ }.to_h
24
+
25
+ new(data)
26
+ end
27
+
28
+ def initialize(data = {})
29
+ @endpoints = data
30
+ @endpoints.default = RuntimeSettings::ProtectionSettings.none
31
+ end
32
+
33
+ # @param route [Aikido::Zen::Route]
34
+ # @return [Aikido::Zen::RuntimeSettings::ProtectionSettings]
35
+ def [](route)
36
+ @endpoints[route]
37
+ end
38
+
39
+ # @!visibility private
40
+ def ==(other)
41
+ other.is_a?(RuntimeSettings::Endpoints) && to_h == other.to_h
42
+ end
43
+
44
+ # @!visibility private
45
+ protected def to_h
46
+ @endpoints
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module Aikido::Zen
6
+ # Models a list of IP addresses or CIDR blocks, where we can check if a given
7
+ # address is part of any of the members.
8
+ class RuntimeSettings::IPSet
9
+ def self.from_json(ips)
10
+ new(Array(ips).map { |ip| IPAddr.new(ip) })
11
+ end
12
+
13
+ def initialize(ips = Set.new)
14
+ @ips = ips.to_set
15
+ end
16
+
17
+ def empty?
18
+ @ips.empty?
19
+ end
20
+
21
+ def include?(ip)
22
+ @ips.any? { |pattern| pattern === ip }
23
+ end
24
+ alias_method :===, :include?
25
+
26
+ def ==(other)
27
+ other.is_a?(RuntimeSettings::IPSet) && to_set == other.to_set
28
+ end
29
+
30
+ protected
31
+
32
+ def to_set
33
+ @ips
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ip_set"
4
+ require_relative "rate_limit_settings"
5
+
6
+ module Aikido::Zen
7
+ # Models the settings for a given Route as configured in the Aikido UI.
8
+ class RuntimeSettings::ProtectionSettings
9
+ # @return [Aikido::Zen::RuntimeSettings::ProtectionSettings] singleton
10
+ # instance for endpoints with no configured protections on a given route,
11
+ # that can be used as a default value for routes.
12
+ def self.none
13
+ @no_settings ||= new
14
+ end
15
+
16
+ # Initialize settings from an API response.
17
+ #
18
+ # @param data [Hash] the deserialized JSON data.
19
+ # @option data [Boolean] "forceProtectionOff" whether the user has
20
+ # disabled attack protection for this route.
21
+ # @option data [Array<String>] "allowedIPAddresses" the list of IPs that
22
+ # can make requests to this endpoint.
23
+ # @option data [Hash] "rateLimiting" the rate limiting options for this
24
+ # endpoint. See {Aikido::Zen::RuntimeSettings::RateLimitSettings.from_json}.
25
+ #
26
+ # @return [Aikido::Zen::RuntimeSettings::ProtectionSettings]
27
+ # @raise [IPAddr::InvalidAddressError] if any of the IPs in
28
+ # "allowedIPAddresses" is not a valid address or family.
29
+ def self.from_json(data)
30
+ ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])
31
+ rate_limiting = RuntimeSettings::RateLimitSettings.from_json(data["rateLimiting"])
32
+
33
+ new(
34
+ protected: !data["forceProtectionOff"],
35
+ allowed_ips: ips,
36
+ rate_limiting: rate_limiting
37
+ )
38
+ end
39
+
40
+ # @return [Aikido::Zen::RuntimeSettings::IPSet] list of IP addresses which
41
+ # are allowed to make requests on this route. If empty, all IP addresses
42
+ # are allowed.
43
+ attr_reader :allowed_ips
44
+
45
+ # @return [Aikido::Zen::RuntimeSettings::RateLimitSettings]
46
+ attr_reader :rate_limiting
47
+
48
+ def initialize(
49
+ protected: true,
50
+ allowed_ips: RuntimeSettings::IPSet.new,
51
+ rate_limiting: RuntimeSettings::RateLimitSettings.disabled
52
+ )
53
+ @protected = !!protected
54
+ @rate_limiting = rate_limiting
55
+ @allowed_ips = allowed_ips
56
+ end
57
+
58
+ def protected?
59
+ @protected
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Simple data object that holds the configuration for rate limiting a given
5
+ # endpoint.
6
+ class RuntimeSettings::RateLimitSettings
7
+ # Initialize the settings from an API response.
8
+ #
9
+ # @param data [Hash] the deserialized JSON data.
10
+ # @option data [Boolean] "enabled"
11
+ # @option data [Integer] "maxRequests"
12
+ # @option data [Integer] "windowSizeInMS"
13
+ #
14
+ # @return [Aikido::Zen::RateLimitSettings]
15
+ def self.from_json(data)
16
+ new(
17
+ enabled: !!data["enabled"],
18
+ max_requests: Integer(data["maxRequests"]),
19
+ period: Integer(data["windowSizeInMS"]) / 1000
20
+ )
21
+ end
22
+
23
+ # Initializes a disabled object that we can use as a default value for
24
+ # endpoints that have not configured rate limiting.
25
+ #
26
+ # @return [Aikido::Zen::RuntimeSettings::RateLimitSettings]
27
+ def self.disabled
28
+ new(enabled: false)
29
+ end
30
+
31
+ # @return [Integer] the fixed window to bucket requests in, in seconds.
32
+ attr_reader :period
33
+
34
+ # @return [Integer]
35
+ attr_reader :max_requests
36
+
37
+ def initialize(enabled: false, max_requests: 1000, period: 60)
38
+ @enabled = enabled
39
+ @period = period
40
+ @max_requests = max_requests
41
+ end
42
+
43
+ def enabled?
44
+ @enabled
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Stores the firewall configuration sourced from the Aikido dashboard. This
5
+ # object is updated by the Agent regularly.
6
+ #
7
+ # Because the RuntimeSettings object can be modified in runtime, it implements
8
+ # the {Observable} API, allowing you to subscribe to updates. These are
9
+ # triggered whenever #update_from_json makes a change (i.e. if the settings
10
+ # don't change, no update is triggered).
11
+ #
12
+ # You can subscribe to changes with +#add_observer(object, func_name)+, which
13
+ # will call the function passing the settings as an argument.
14
+ class RuntimeSettings < Concurrent::MutableStruct.new(
15
+ :updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :skip_protection_for_ips, :received_any_stats
16
+ )
17
+ include Concurrent::Concern::Observable
18
+
19
+ def initialize(*)
20
+ self.observers = Concurrent::Collection::CopyOnWriteObserverSet.new
21
+ super
22
+ self.endpoints ||= Endpoints.new
23
+ self.skip_protection_for_ips ||= IPSet.new
24
+ end
25
+
26
+ # @!attribute [rw] updated_at
27
+ # @return [Time] when these settings were updated in the Aikido dashboard.
28
+
29
+ # @!attribute [rw] heartbeat_interval
30
+ # @return [Integer] duration in seconds between heartbeat requests to the
31
+ # Aikido server.
32
+
33
+ # @!attribute [rw] received_any_stats
34
+ # @return [Boolean] whether the Aikido server has received any data from
35
+ # this application.
36
+
37
+ # @!attribute [rw] endpoints
38
+ # @return [Aikido::Zen::RuntimeSettings::Endpoints]
39
+
40
+ # @!attribute [rw] blocked_user_ids
41
+ # @return [Array]
42
+
43
+ # @!attribute [rw] skip_protection_for_ips
44
+ # @return [Aikido::Zen::RuntimeSettings::IPSet]
45
+
46
+ # Parse and interpret the JSON response from the core API with updated
47
+ # settings, and apply the changes. This will also notify any subscriber
48
+ # to updates
49
+ #
50
+ # @param data [Hash] the decoded JSON payload from the /api/runtime/config
51
+ # API endpoint.
52
+ #
53
+ # @return [void]
54
+ def update_from_json(data)
55
+ last_updated_at = updated_at
56
+
57
+ self.updated_at = Time.at(data["configUpdatedAt"].to_i / 1000)
58
+ self.heartbeat_interval = (data["heartbeatIntervalInMS"].to_i / 1000)
59
+ self.endpoints = Endpoints.from_json(data["endpoints"])
60
+ self.blocked_user_ids = data["blockedUserIds"]
61
+ self.skip_protection_for_ips = IPSet.from_json(data["allowedIPAddresses"])
62
+ self.received_any_stats = data["receivedAnyStats"]
63
+
64
+ observers.notify_observers(self) if updated_at != last_updated_at
65
+ end
66
+ end
67
+ end
68
+
69
+ require_relative "runtime_settings/ip_set"
70
+ require_relative "runtime_settings/endpoints"
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Scans track information about a single call made by one of our Sinks
5
+ # including whether it was detected as an attack or how long it took.
6
+ class Scan
7
+ # @return [Aikido::Zen::Sink] the originating Sink.
8
+ attr_reader :sink
9
+
10
+ # @return [Aikido::Zen::Context] the current Context, wrapping the HTTP
11
+ # request during which this scan was performed.
12
+ attr_reader :context
13
+
14
+ # @return [Aikido::Zen::Attack, nil] a detected Attack, or
15
+ # +nil+ if the scan was considered safe.
16
+ attr_reader :attack
17
+
18
+ # @return [Float, nil] duration in (fractional) seconds of the scan.
19
+ attr_reader :duration
20
+
21
+ # @return [Array<Hash>] list of captured exceptions while scanning.
22
+ attr_reader :errors
23
+
24
+ # @param sink [Aikido::Zen::Sink]
25
+ # @param context [Aikido::Zen::Context]
26
+ def initialize(sink:, context:)
27
+ @sink = sink
28
+ @context = context
29
+ @errors = []
30
+ @performed = false
31
+ end
32
+
33
+ def performed?
34
+ @performed
35
+ end
36
+
37
+ # @return [Boolean] whether this scan detected an Attack.
38
+ def attack?
39
+ @attack != nil
40
+ end
41
+
42
+ # @return [Boolean] whether any errors were caught by this Scan.
43
+ def errors?
44
+ @errors.any?
45
+ end
46
+
47
+ # Runs a block of code, capturing its return value as the potential
48
+ # Attack object (or nil, if safe), and how long it took to run.
49
+ #
50
+ # @yieldreturn [Aikido::Zen::Attack, nil]
51
+ # @return [void]
52
+ def perform
53
+ @performed = true
54
+ started_at = monotonic_time
55
+ @attack = yield
56
+ ensure
57
+ @duration = monotonic_time - started_at
58
+ end
59
+
60
+ # Keep track of exceptions encountered during scanning.
61
+ #
62
+ # @param error [Exception]
63
+ # @param scanner [#call]
64
+ #
65
+ # @return [nil]
66
+ def track_error(error, scanner)
67
+ @errors << {error: error, scanner: scanner}
68
+ nil
69
+ end
70
+
71
+ private def monotonic_time
72
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../attack"
4
+ require_relative "../internals"
5
+
6
+ module Aikido::Zen
7
+ module Scanners
8
+ class SQLInjectionScanner
9
+ # Checks if the given SQL query may have dangerous user input injected,
10
+ # and returns an Attack if so, based on the current request.
11
+ #
12
+ # @param query [String]
13
+ # @param context [Aikido::Zen::Context]
14
+ # @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
15
+ # @param dialect [Symbol] one of +:mysql+, +:postgesql+, or +:sqlite+.
16
+ # @param operation [Symbol, String] name of the method being scanned.
17
+ # Expects +sink.operation+ being set to get the full module/name combo.
18
+ #
19
+ # @return [Aikido::Zen::Attack, nil] an Attack if any user input is
20
+ # detected to be attempting a SQL injection, or nil if this is safe.
21
+ #
22
+ # @raise [Aikido::Zen::InternalsError] if an error occurs when loading or
23
+ # calling zenlib. See Sink#scan.
24
+ def self.call(query:, dialect:, sink:, context:, operation:)
25
+ # FIXME: This assumes queries executed outside of an HTTP request are
26
+ # safe, but this is not the case. For example, if an HTTP request
27
+ # enqueues a background job, passing user input verbatim, the job might
28
+ # pass that input to a query without having a current request in scope.
29
+ return if context.nil?
30
+
31
+ dialect = DIALECTS.fetch(dialect) do
32
+ Aikido::Zen.config.logger.warn "Unknown SQL dialect #{dialect.inspect}"
33
+ DIALECTS[:common]
34
+ end
35
+
36
+ context.payloads.each do |payload|
37
+ next unless new(query, payload.value, dialect).attack?
38
+
39
+ return Attacks::SQLInjectionAttack.new(
40
+ sink: sink,
41
+ query: query,
42
+ input: payload,
43
+ dialect: dialect,
44
+ context: context,
45
+ operation: "#{sink.operation}.#{operation}"
46
+ )
47
+ end
48
+
49
+ nil
50
+ end
51
+
52
+ def initialize(query, input, dialect)
53
+ @query = query.downcase
54
+ @input = input.downcase
55
+ @dialect = dialect
56
+ end
57
+
58
+ def attack?
59
+ # Ignore single char inputs since they shouldn't be able to do much harm
60
+ return false if @input.length <= 1
61
+
62
+ # If the input is longer than the query, then it is not part of it
63
+ return false if @input.length > @query.length
64
+
65
+ # If the input is not included in the query at all, then we are safe
66
+ return false unless @query.include?(@input)
67
+
68
+ # If the input is solely alphanumeric, we can ignore it
69
+ return false if /\A[[:alnum:]_]+\z/i.match?(@input)
70
+
71
+ # If the input is a comma-separated list of numbers, ignore it.
72
+ return false if /\A(?:\d+(?:,\s*)?)+\z/i.match?(@input)
73
+
74
+ Internals.detect_sql_injection(@query, @input, @dialect)
75
+ end
76
+
77
+ # @api private
78
+ Dialect = Struct.new(:name, :internals_key, keyword_init: true) do
79
+ alias_method :to_s, :name
80
+ alias_method :to_int, :internals_key
81
+ end
82
+
83
+ # Maps easy-to-use Symbols to a struct that keeps both the name and the
84
+ # internal identifier used by libzen.
85
+ #
86
+ # @see https://github.com/AikidoSec/zen-internals/blob/main/src/sql_injection/helpers/select_dialect_based_on_enum.rs
87
+ DIALECTS = {
88
+ common: Dialect.new(name: "SQL", internals_key: 0),
89
+ mysql: Dialect.new(name: "MySQL", internals_key: 8),
90
+ postgresql: Dialect.new(name: "PostgreSQL", internals_key: 9),
91
+ sqlite: Dialect.new(name: "SQLite", internals_key: 12)
92
+ }
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ module Aikido::Zen
6
+ module Scanners
7
+ module SSRF
8
+ # Simple per-request cache of all DNS lookups performed for a given host.
9
+ # We can store this in the context after performing a lookup, and have the
10
+ # SSRF scanner make sure the hostname being inspected doesn't actually
11
+ # resolve to an internal/dangerous IP.
12
+ class DNSLookups < SimpleDelegator
13
+ def initialize
14
+ super(Hash.new { |h, k| h[k] = [] })
15
+ end
16
+
17
+ def add(hostname, addresses)
18
+ self[hostname].concat(Array(addresses))
19
+ end
20
+
21
+ def ===(hostname)
22
+ key?(hostname)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+ require "ipaddr"
5
+
6
+ module Aikido::Zen
7
+ module Scanners
8
+ module SSRF
9
+ # Little helper to check if a given hostname or address is to be
10
+ # considered "dangerous" when used for an outbound HTTP request.
11
+ #
12
+ # When given a hostname:
13
+ #
14
+ # * If any DNS lookups have been performed and stored in the current Zen
15
+ # context (under the "dns.lookups" metadata key), we will map it to the
16
+ # list of IPs that we've resolved it to.
17
+ #
18
+ # * If not, we'll still try to map it to any statically defined address in
19
+ # the system hosts file (e.g. /etc/hosts).
20
+ #
21
+ # Once we mapped the hostname to an IP address (or, if given an IP
22
+ # address), this will check that it's not a loopback address, a private IP
23
+ # address (as defined by RFCs 1918 and 4193), or in one of the
24
+ # "special-use" IP ranges defined in RFC 5735.
25
+ class PrivateIPChecker
26
+ def initialize(resolver = Resolv::Hosts.new)
27
+ @resolver = resolver
28
+ end
29
+
30
+ # @param hostname_or_address [String]
31
+ # @return [Boolean]
32
+ def private?(hostname_or_address)
33
+ resolve(hostname_or_address).any? do |ip|
34
+ ip.loopback? || ip.private? || RFC5735.any? { |range| range === ip }
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ RFC5735 = [
41
+ IPAddr.new("0.0.0.0/8"),
42
+ IPAddr.new("100.64.0.0/10"),
43
+ IPAddr.new("127.0.0.0/8"),
44
+ IPAddr.new("169.254.0.0/16"),
45
+ IPAddr.new("192.0.0.0/24"),
46
+ IPAddr.new("192.0.2.0/24"),
47
+ IPAddr.new("192.31.196.0/24"),
48
+ IPAddr.new("192.52.193.0/24"),
49
+ IPAddr.new("192.88.99.0/24"),
50
+ IPAddr.new("192.175.48.0/24"),
51
+ IPAddr.new("198.18.0.0/15"),
52
+ IPAddr.new("198.51.100.0/24"),
53
+ IPAddr.new("203.0.113.0/24"),
54
+ IPAddr.new("240.0.0.0/4"),
55
+ IPAddr.new("224.0.0.0/4"),
56
+ IPAddr.new("255.255.255.255/32"),
57
+
58
+ IPAddr.new("::/128"), # Unspecified address
59
+ IPAddr.new("fe80::/10"), # Link-local address (LLA)
60
+ IPAddr.new("::ffff:127.0.0.1/128") # IPv4-mapped address
61
+ ]
62
+
63
+ def resolved_in_current_context
64
+ context = Aikido::Zen.current_context
65
+ context && context["dns.lookups"]
66
+ end
67
+
68
+ def resolve(hostname_or_address)
69
+ return [] if hostname_or_address.nil?
70
+
71
+ case hostname_or_address
72
+ when Resolv::AddressRegex
73
+ [IPAddr.new(hostname_or_address)]
74
+ when resolved_in_current_context
75
+ resolved_in_current_context[hostname_or_address]
76
+ .map { |address| IPAddr.new(address) }
77
+ else
78
+ @resolver.getaddresses(hostname_or_address.to_s)
79
+ .map { |address| IPAddr.new(address) }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end