aikido-zen 1.0.1.beta.2-arm64-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.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.aikido +6 -0
  3. data/.ruby-version +1 -0
  4. data/.simplecov +26 -0
  5. data/.standard.yml +3 -0
  6. data/LICENSE +674 -0
  7. data/README.md +146 -0
  8. data/Rakefile +67 -0
  9. data/benchmarks/README.md +23 -0
  10. data/benchmarks/rails7.1_sql_injection.js +70 -0
  11. data/docs/banner.svg +202 -0
  12. data/docs/config.md +125 -0
  13. data/docs/rails.md +70 -0
  14. data/lib/aikido/zen/actor.rb +116 -0
  15. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  16. data/lib/aikido/zen/agent.rb +179 -0
  17. data/lib/aikido/zen/api_client.rb +142 -0
  18. data/lib/aikido/zen/attack.rb +207 -0
  19. data/lib/aikido/zen/background_worker.rb +52 -0
  20. data/lib/aikido/zen/capped_collections.rb +68 -0
  21. data/lib/aikido/zen/collector/hosts.rb +15 -0
  22. data/lib/aikido/zen/collector/routes.rb +66 -0
  23. data/lib/aikido/zen/collector/sink_stats.rb +95 -0
  24. data/lib/aikido/zen/collector/stats.rb +111 -0
  25. data/lib/aikido/zen/collector/users.rb +30 -0
  26. data/lib/aikido/zen/collector.rb +144 -0
  27. data/lib/aikido/zen/config.rb +279 -0
  28. data/lib/aikido/zen/context/rack_request.rb +24 -0
  29. data/lib/aikido/zen/context/rails_request.rb +42 -0
  30. data/lib/aikido/zen/context.rb +112 -0
  31. data/lib/aikido/zen/detached_agent/agent.rb +78 -0
  32. data/lib/aikido/zen/detached_agent/front_object.rb +37 -0
  33. data/lib/aikido/zen/detached_agent/server.rb +41 -0
  34. data/lib/aikido/zen/detached_agent.rb +2 -0
  35. data/lib/aikido/zen/errors.rb +107 -0
  36. data/lib/aikido/zen/event.rb +71 -0
  37. data/lib/aikido/zen/internals.rb +102 -0
  38. data/lib/aikido/zen/libzen-v0.1.39-arm64-linux-musl.so +0 -0
  39. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +26 -0
  40. data/lib/aikido/zen/middleware/middleware.rb +11 -0
  41. data/lib/aikido/zen/middleware/rack_throttler.rb +48 -0
  42. data/lib/aikido/zen/middleware/request_tracker.rb +192 -0
  43. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  44. data/lib/aikido/zen/outbound_connection.rb +45 -0
  45. data/lib/aikido/zen/outbound_connection_monitor.rb +23 -0
  46. data/lib/aikido/zen/package.rb +22 -0
  47. data/lib/aikido/zen/payload.rb +50 -0
  48. data/lib/aikido/zen/rails_engine.rb +70 -0
  49. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  50. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  51. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  52. data/lib/aikido/zen/rate_limiter.rb +50 -0
  53. data/lib/aikido/zen/request/heuristic_router.rb +115 -0
  54. data/lib/aikido/zen/request/rails_router.rb +72 -0
  55. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  56. data/lib/aikido/zen/request/schema/auth_schemas.rb +54 -0
  57. data/lib/aikido/zen/request/schema/builder.rb +121 -0
  58. data/lib/aikido/zen/request/schema/definition.rb +107 -0
  59. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  60. data/lib/aikido/zen/request/schema.rb +87 -0
  61. data/lib/aikido/zen/request.rb +103 -0
  62. data/lib/aikido/zen/route.rb +39 -0
  63. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  64. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  65. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  66. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  67. data/lib/aikido/zen/runtime_settings.rb +65 -0
  68. data/lib/aikido/zen/scan.rb +75 -0
  69. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +65 -0
  70. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +63 -0
  71. data/lib/aikido/zen/scanners/shell_injection/helpers.rb +159 -0
  72. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +64 -0
  73. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +93 -0
  74. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  75. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +97 -0
  76. data/lib/aikido/zen/scanners/ssrf_scanner.rb +265 -0
  77. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +49 -0
  78. data/lib/aikido/zen/scanners.rb +7 -0
  79. data/lib/aikido/zen/sink.rb +118 -0
  80. data/lib/aikido/zen/sinks/action_controller.rb +83 -0
  81. data/lib/aikido/zen/sinks/async_http.rb +82 -0
  82. data/lib/aikido/zen/sinks/curb.rb +115 -0
  83. data/lib/aikido/zen/sinks/em_http.rb +85 -0
  84. data/lib/aikido/zen/sinks/excon.rb +121 -0
  85. data/lib/aikido/zen/sinks/file.rb +116 -0
  86. data/lib/aikido/zen/sinks/http.rb +95 -0
  87. data/lib/aikido/zen/sinks/httpclient.rb +97 -0
  88. data/lib/aikido/zen/sinks/httpx.rb +80 -0
  89. data/lib/aikido/zen/sinks/kernel.rb +34 -0
  90. data/lib/aikido/zen/sinks/mysql2.rb +33 -0
  91. data/lib/aikido/zen/sinks/net_http.rb +103 -0
  92. data/lib/aikido/zen/sinks/patron.rb +105 -0
  93. data/lib/aikido/zen/sinks/pg.rb +74 -0
  94. data/lib/aikido/zen/sinks/resolv.rb +62 -0
  95. data/lib/aikido/zen/sinks/socket.rb +80 -0
  96. data/lib/aikido/zen/sinks/sqlite3.rb +49 -0
  97. data/lib/aikido/zen/sinks/trilogy.rb +33 -0
  98. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  99. data/lib/aikido/zen/sinks.rb +39 -0
  100. data/lib/aikido/zen/sinks_dsl.rb +226 -0
  101. data/lib/aikido/zen/synchronizable.rb +24 -0
  102. data/lib/aikido/zen/system_info.rb +84 -0
  103. data/lib/aikido/zen/version.rb +10 -0
  104. data/lib/aikido/zen/worker.rb +87 -0
  105. data/lib/aikido/zen.rb +206 -0
  106. data/lib/aikido-zen.rb +3 -0
  107. data/placeholder/.gitignore +4 -0
  108. data/placeholder/README.md +11 -0
  109. data/placeholder/Rakefile +75 -0
  110. data/placeholder/lib/placeholder.rb.template +3 -0
  111. data/placeholder/placeholder.gemspec.template +20 -0
  112. data/tasklib/bench.rake +94 -0
  113. data/tasklib/libzen.rake +132 -0
  114. data/tasklib/wrk.rb +88 -0
  115. 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
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen::Middleware
4
+ def self.request_from(env)
5
+ if (current_context = Aikido::Zen.current_context)
6
+ current_context.request
7
+ else
8
+ Aikido::Zen::Context.from_rack_env(env).request
9
+ end
10
+ end
11
+ 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