aikido-zen 0.1.0.alpha4-arm64-darwin

Sign up to get free protection for your applications and to get access to all the features.
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