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.
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-x86_64-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,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+ require_relative "config"
5
+
6
+ module Aikido::Zen
7
+ # Converts an object into an Actor for reporting back to the Aikido Dashboard.
8
+ #
9
+ # @overload Actor(actor)
10
+ # @param actor [#to_aikido_actor] anything that implements #to_aikido_actor
11
+ # will have that method called and its value returned.
12
+ # @return Aikido::Zen::Actor
13
+ #
14
+ # @overload Actor(data)
15
+ # @param data [Hash<Symbol, String>]
16
+ # @option data [String] :id a unique identifier for this user.
17
+ # @option data [String, nil] :name an optional name to display in the UI.
18
+ # @return Aikido::Zen::Actor
19
+ def self.Actor(data)
20
+ return if data.nil?
21
+ return data.to_aikido_actor if data.respond_to?(:to_aikido_actor)
22
+
23
+ attrs = {}
24
+ if data.respond_to?(:to_hash)
25
+ attrs = data.to_hash
26
+ .slice("id", "name", :id, :name)
27
+ .compact
28
+ .transform_keys(&:to_sym)
29
+ .transform_values(&:to_s)
30
+ else
31
+ return nil
32
+ end
33
+
34
+ return nil if attrs[:id].nil? || attrs[:id].to_s.strip.empty?
35
+
36
+ Actor.new(**attrs)
37
+ end
38
+
39
+ # Represents someone connecting to the application and making requests.
40
+ class Actor
41
+ # @return [String] a unique identifier for this user.
42
+ attr_reader :id
43
+
44
+ # @return [String, nil] an optional name to display in the Aikido UI.
45
+ attr_reader :name
46
+
47
+ # @return [Time]
48
+ attr_reader :first_seen_at
49
+
50
+ # @param id [String]
51
+ # @param name [String, nil]
52
+ # @param ip [String, nil]
53
+ # @param seen_at [Time]
54
+ def initialize(
55
+ id:,
56
+ name: nil,
57
+ ip: Aikido::Zen.current_context&.request&.ip,
58
+ seen_at: Time.now.utc
59
+ )
60
+ @id = id
61
+ @name = name
62
+ @first_seen_at = seen_at
63
+ @last_seen_at = Concurrent::AtomicReference.new(seen_at)
64
+ @ip = Concurrent::AtomicReference.new(ip)
65
+ end
66
+
67
+ # @return [Time]
68
+ def last_seen_at
69
+ @last_seen_at.get
70
+ end
71
+
72
+ # @return [String, nil] the IP address last used by this user, if available.
73
+ def ip
74
+ @ip.get
75
+ end
76
+
77
+ # Atomically update the last IP used by the user, and the last time they've
78
+ # been "seen" connecting to the app.
79
+ #
80
+ # @param ip [String, nil] the last-seen IP address for the user. If +nil+
81
+ # and we had a non-empty value before, we won't update it. Defaults to
82
+ # the current HTTP request's IP address, if any.
83
+ # @param seen_at [Time] the time at which we're making the update. We will
84
+ # always keep the most recent time if this conflicts with the current
85
+ # value.
86
+ # @return [void]
87
+ def update(seen_at: Time.now.utc, ip: Aikido::Zen.current_context&.request&.ip)
88
+ @last_seen_at.try_update { |last_seen_at| [last_seen_at, seen_at].max }
89
+ @ip.try_update { |last_ip| [ip, last_ip].compact.first }
90
+ end
91
+
92
+ # @return [self]
93
+ def to_aikido_actor
94
+ self
95
+ end
96
+
97
+ def ==(other)
98
+ other.is_a?(Actor) && id == other.id
99
+ end
100
+ alias_method :eql?, :==
101
+
102
+ def hash
103
+ id.hash
104
+ end
105
+
106
+ def as_json
107
+ {
108
+ id: id,
109
+ name: name,
110
+ lastIpAddress: ip,
111
+ firstSeenAt: first_seen_at.to_i * 1000,
112
+ lastSeenAt: last_seen_at.to_i * 1000
113
+ }.compact
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Handles scheduling the heartbeats we send to the Aikido servers, managing
5
+ # runtime changes to the heartbeat interval.
6
+ class Agent::HeartbeatsManager
7
+ def initialize(worker:, settings: Aikido::Zen.runtime_settings, config: Aikido::Zen.config)
8
+ @settings = settings
9
+ @config = config
10
+ @worker = worker
11
+
12
+ @timer = nil
13
+ end
14
+
15
+ # @return [Boolean]
16
+ def running?
17
+ !!@timer&.running?
18
+ end
19
+
20
+ # @return [Boolean] whether the currently running heartbeat matches the
21
+ # expected interval in the runtime settings.
22
+ def stale_settings?
23
+ running? && @timer.execution_interval != @settings.heartbeat_interval
24
+ end
25
+
26
+ # Sets up the the timer to run the given block at the appropriate interval.
27
+ # Re-entrant, and does nothing if already running.
28
+ #
29
+ # @return [void]
30
+ def start(&task)
31
+ return if running?
32
+
33
+ if @settings.heartbeat_interval&.nonzero?
34
+ @config.logger.debug "Scheduling heartbeats every #{@settings.heartbeat_interval} seconds"
35
+ @timer = @worker.every(@settings.heartbeat_interval, run_now: false, &task)
36
+ else
37
+ @config.logger.warn(format("Heartbeat could not be set up (interval: %p)", @settings.heartbeat_interval))
38
+ end
39
+ end
40
+
41
+ # Cleans up the timer.
42
+ #
43
+ # @return [void]
44
+ def stop
45
+ return unless running?
46
+
47
+ @timer.shutdown
48
+ @timer = nil
49
+ end
50
+
51
+ # Resets the timer to start with any new settings, if needed.
52
+ #
53
+ # @return [void]
54
+ def restart(&task)
55
+ stop
56
+ start(&task)
57
+ end
58
+
59
+ # @api private
60
+ #
61
+ # @return [Integer] the current delay between events.
62
+ def interval
63
+ @settings.heartbeat_interval
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+ require_relative "event"
5
+ require_relative "config"
6
+ require_relative "system_info"
7
+
8
+ module Aikido::Zen
9
+ # Handles the background processes that communicate with the Aikido servers,
10
+ # including managing the runtime settings that keep the app protected.
11
+ class Agent
12
+ # Initialize and start an agent instance.
13
+ #
14
+ # @return [Aikido::Zen::Agent]
15
+ def self.start(**opts)
16
+ new(**opts).tap(&:start!)
17
+ end
18
+
19
+ def initialize(
20
+ config: Aikido::Zen.config,
21
+ collector: Aikido::Zen.collector,
22
+ detached_agent: Aikido::Zen.detached_agent,
23
+ worker: Aikido::Zen::Worker.new(config: config),
24
+ api_client: Aikido::Zen::APIClient.new(config: config)
25
+ )
26
+ @started_at = nil
27
+
28
+ @config = config
29
+ @worker = worker
30
+ @api_client = api_client
31
+ @collector = collector
32
+ @detached_agent = detached_agent
33
+ end
34
+
35
+ def started?
36
+ !!@started_at
37
+ end
38
+
39
+ def start!
40
+ @config.logger.info "Starting Aikido agent v#{Aikido::Zen::VERSION}"
41
+
42
+ raise Aikido::ZenError, "Aikido Agent already started!" if started?
43
+ @started_at = Time.now.utc
44
+ @collector.start(at: @started_at)
45
+
46
+ if @config.blocking_mode?
47
+ @config.logger.info "Requests identified as attacks will be blocked"
48
+ else
49
+ @config.logger.warn "Non-blocking mode enabled! No requests will be blocked."
50
+ end
51
+
52
+ if @api_client.can_make_requests?
53
+ @config.logger.info "API Token set! Reporting has been enabled."
54
+ else
55
+ @config.logger.warn "No API Token set! Reporting has been disabled."
56
+ return
57
+ end
58
+
59
+ at_exit { stop! if started? }
60
+
61
+ report(Events::Started.new(time: @started_at)) do |response|
62
+ updated_settings! if Aikido::Zen.runtime_settings.update_from_json(response)
63
+ @config.logger.info "Updated runtime settings."
64
+ rescue => err
65
+ @config.logger.error(err.message)
66
+ end
67
+
68
+ poll_for_setting_updates
69
+
70
+ @worker.delay(@config.initial_heartbeat_delay) do
71
+ send_heartbeat if @collector.stats.any?
72
+ end
73
+ end
74
+
75
+ # Clean up any ongoing threads, and reset the state. Called automatically
76
+ # when the process exits.
77
+ #
78
+ # @return [void]
79
+ def stop!
80
+ @config.logger.info "Stopping Aikido agent"
81
+ @started_at = nil
82
+ @worker.shutdown
83
+ end
84
+
85
+ # Respond to the runtime settings changing after being fetched from the
86
+ # Aikido servers.
87
+ #
88
+ # @return [void]
89
+ def updated_settings!
90
+ if !heartbeats.running?
91
+ heartbeats.start { send_heartbeat }
92
+ elsif heartbeats.stale_settings?
93
+ heartbeats.restart { send_heartbeat }
94
+ end
95
+ end
96
+
97
+ # Given an Attack, report it to the Aikido server, and/or block the request
98
+ # depending on configuration.
99
+ #
100
+ # @param attack [Attack] a detected attack.
101
+ # @return [void]
102
+ #
103
+ # @raise [Aikido::Zen::UnderAttackError] if the firewall is configured
104
+ # to block requests.
105
+ def handle_attack(attack)
106
+ attack.will_be_blocked! if @config.blocking_mode?
107
+
108
+ @config.logger.error(
109
+ format("Zen has %s a %s: %s", attack.blocked? ? "blocked" : "detected", attack.humanized_name, attack.as_json.to_json)
110
+ )
111
+ report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?
112
+
113
+ @collector.track_attack(attack)
114
+ raise attack if attack.blocked?
115
+ end
116
+
117
+ # Asynchronously reports an Event of any kind to the Aikido dashboard. If
118
+ # given a block, the API response will be passed to the block for handling.
119
+ #
120
+ # @param event [Aikido::Zen::Event]
121
+ # @yieldparam response [Object] the response from the reporting API in case
122
+ # of a successful request.
123
+ #
124
+ # @return [void]
125
+ def report(event)
126
+ @worker.perform do
127
+ response = @api_client.report(event)
128
+ yield response if response && block_given?
129
+ rescue Aikido::Zen::APIError, Aikido::Zen::NetworkError => err
130
+ @config.logger.error(err.message)
131
+ end
132
+ end
133
+
134
+ # @api private
135
+ #
136
+ # Atomically flushes all the stats stored by the agent, and sends a
137
+ # heartbeat event. Scheduled to run automatically on a recurring schedule
138
+ # when reporting is enabled.
139
+ #
140
+ # @param at [Time] the event time. Defaults to now.
141
+ # @return [void]
142
+ # @see Aikido::Zen::RuntimeSettings#heartbeat_interval
143
+ def send_heartbeat(at: Time.now.utc)
144
+ return unless @api_client.can_make_requests?
145
+
146
+ @collector.flush_heartbeats.each do |heartbeat|
147
+ report(heartbeat) do |response|
148
+ updated_settings! if Aikido::Zen.runtime_settings.update_from_json(response)
149
+ @config.logger.info "Updated runtime settings after heartbeat"
150
+ end
151
+ end
152
+ end
153
+
154
+ # @api private
155
+ #
156
+ # Sets up the timer task that polls the Aikido Runtime API for updates to
157
+ # the runtime settings every minute.
158
+ #
159
+ # @return [void]
160
+ # @see Aikido::Zen::RuntimeSettings
161
+ def poll_for_setting_updates
162
+ @worker.every(@config.polling_interval) do
163
+ if @api_client.should_fetch_settings?
164
+ updated_settings! if Aikido::Zen.runtime_settings.update_from_json(@api_client.fetch_settings)
165
+ @config.logger.info "Updated runtime settings after polling"
166
+ end
167
+ end
168
+ end
169
+
170
+ private def heartbeats
171
+ @heartbeats ||= Aikido::Zen::Agent::HeartbeatsManager.new(
172
+ config: @config,
173
+ worker: @worker
174
+ )
175
+ end
176
+ end
177
+ end
178
+
179
+ require_relative "agent/heartbeats_manager"
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require_relative "rate_limiter"
5
+
6
+ module Aikido::Zen
7
+ # Implements all communication with the Aikido servers.
8
+ class APIClient
9
+ def initialize(
10
+ config: Aikido::Zen.config,
11
+ rate_limiter: Aikido::Zen::RateLimiter::Breaker.new,
12
+ system_info: Aikido::Zen.system_info
13
+ )
14
+ @config = config
15
+ @system_info = system_info
16
+ @rate_limiter = rate_limiter
17
+ end
18
+
19
+ # @return [Boolean] whether we have a configured token.
20
+ def can_make_requests?
21
+ @config.api_token.to_s.size > 0
22
+ end
23
+
24
+ # Checks with the Aikido Runtime API the timestamp of the last settings
25
+ # update, and compares against the given value.
26
+ #
27
+ # @param last_updated_at [Time]
28
+ #
29
+ # @return [Boolean]
30
+ # @raise (see #request)
31
+ def should_fetch_settings?(last_updated_at = Aikido::Zen.runtime_settings.updated_at)
32
+ @config.logger.debug("Polling for new runtime settings to fetch")
33
+
34
+ return false unless can_make_requests?
35
+ return true if last_updated_at.nil?
36
+
37
+ response = request(
38
+ Net::HTTP::Get.new("/config", default_headers),
39
+ base_url: @config.realtime_endpoint
40
+ )
41
+
42
+ new_updated_at = Time.at(response["configUpdatedAt"].to_i / 1000)
43
+ new_updated_at > last_updated_at
44
+ end
45
+
46
+ # Fetches the runtime settings from the server. In case of a timeout or
47
+ # other low-lever error, the request will be automatically retried up to two
48
+ # times, after which it will raise an error.
49
+ #
50
+ # @return [Hash] decoded JSON response from the server with the runtime
51
+ # settings.
52
+ # @raise (see #request)
53
+ def fetch_settings
54
+ @config.logger.debug("Fetching new runtime settings")
55
+
56
+ request(Net::HTTP::Get.new("/api/runtime/config", default_headers))
57
+ end
58
+
59
+ # @overload report(event)
60
+ # Reports an event to the server.
61
+ #
62
+ # @param event [Aikido::Zen::Event]
63
+ # @return [void]
64
+ # @raise (see #request)
65
+ #
66
+ # @overload report(settings_updating_event)
67
+ # Reports an event that responds with updated runtime settings, and
68
+ # requires us to update settings afterwards.
69
+ #
70
+ # @param settings_updating_event [Aikido::Zen::Events::Started,
71
+ # Aikido::Zen::Events::Heartbeat]
72
+ # @return (see #fetch_settings)
73
+ # @raise (see #request)
74
+ def report(event)
75
+ event_type = if event.respond_to?(:type)
76
+ event.type
77
+ else
78
+ event[:type]
79
+ end
80
+
81
+ if @rate_limiter.throttle?(event_type)
82
+ @config.logger.error("Not reporting #{event_type.upcase} event due to rate limiting")
83
+ return
84
+ end
85
+
86
+ @config.logger.debug("Reporting #{event_type.upcase} event")
87
+
88
+ req = Net::HTTP::Post.new("/api/runtime/events", default_headers)
89
+ req.content_type = "application/json"
90
+ req.body = if event.respond_to?(:as_json)
91
+ @config.json_encoder.call(event.as_json)
92
+ else
93
+ @config.json_encoder.call(event)
94
+ end
95
+
96
+ request(req)
97
+ rescue Aikido::Zen::RateLimitedError
98
+ @rate_limiter.open!
99
+ raise
100
+ end
101
+
102
+ # Perform an HTTP request against one of our API endpoints, and process the
103
+ # response.
104
+ #
105
+ # @param request [Net::HTTPRequest]
106
+ # @param base_url [URI] which API to use. Defaults to +Config#api_endpoint+.
107
+ #
108
+ # @return [Object] the result of decoding the JSON response from the server.
109
+ #
110
+ # @raise [Aikido::Zen::APIError] in case of a 4XX or 5XX response.
111
+ # @raise [Aikido::Zen::NetworkError] if an error occurs trying to make the
112
+ # request.
113
+ private def request(request, base_url: @config.api_endpoint)
114
+ Net::HTTP.start(base_url.host, base_url.port, http_settings) do |http|
115
+ response = http.request(request)
116
+
117
+ case response
118
+ when Net::HTTPSuccess
119
+ @config.json_decoder.call(response.body)
120
+ when Net::HTTPTooManyRequests
121
+ raise RateLimitedError.new(request, response)
122
+ else
123
+ raise APIError.new(request, response)
124
+ end
125
+ end
126
+ rescue Timeout::Error, IOError, SystemCallError, OpenSSL::OpenSSLError => err
127
+ raise NetworkError.new(request, err)
128
+ end
129
+
130
+ private def http_settings
131
+ @http_settings ||= {use_ssl: true, max_retries: 2}.merge(@config.api_timeouts)
132
+ end
133
+
134
+ private def default_headers
135
+ @default_headers ||= {
136
+ "Authorization" => @config.api_token,
137
+ "Accept" => "application/json",
138
+ "User-Agent" => "#{@system_info.library_name} v#{@system_info.library_version}"
139
+ }
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # Attack objects gather information about a type of detected attack.
5
+ # They can be used in a few ways, like for reporting an attack event
6
+ # to the Aikido server, or can be raised as errors to block requests
7
+ # if blocking_mode is on.
8
+ class Attack
9
+ attr_reader :context
10
+ attr_reader :operation
11
+ attr_accessor :sink
12
+
13
+ def initialize(context:, sink:, operation:)
14
+ @context = context
15
+ @operation = operation
16
+ @sink = sink
17
+ @blocked = false
18
+ end
19
+
20
+ def will_be_blocked!
21
+ @blocked = true
22
+ end
23
+
24
+ def blocked?
25
+ @blocked
26
+ end
27
+
28
+ def humanized_name
29
+ raise NotImplementedError, "implement in subclasses"
30
+ end
31
+
32
+ def kind
33
+ raise NotImplementedError, "implement in subclasses"
34
+ end
35
+
36
+ def input
37
+ raise NotImplementedError, "implement in subclasses"
38
+ end
39
+
40
+ def metadata
41
+ raise NotImplementedError, "implement in subclasses"
42
+ end
43
+
44
+ def as_json
45
+ {
46
+ kind: kind,
47
+ blocked: blocked?,
48
+ metadata: metadata,
49
+ operation: @operation
50
+ }.merge(input.as_json)
51
+ end
52
+
53
+ def exception(*)
54
+ raise NotImplementedError, "implement in subclasses"
55
+ end
56
+ end
57
+
58
+ module Attacks
59
+ class PathTraversalAttack < Attack
60
+ attr_reader :input
61
+ attr_reader :filepath
62
+
63
+ def initialize(input:, filepath:, **opts)
64
+ super(**opts)
65
+ @input = input
66
+ @filepath = filepath
67
+ end
68
+
69
+ def metadata
70
+ {filename: filepath}
71
+ end
72
+
73
+ def humanized_name
74
+ "path traversal attack"
75
+ end
76
+
77
+ def kind
78
+ "path_traversal"
79
+ end
80
+
81
+ def exception(*)
82
+ PathTraversalError.new(self)
83
+ end
84
+ end
85
+
86
+ class ShellInjectionAttack < Attack
87
+ attr_reader :input
88
+ attr_reader :command
89
+
90
+ def initialize(input:, command:, **opts)
91
+ super(**opts)
92
+ @input = input
93
+ @command = command
94
+ end
95
+
96
+ def humanized_name
97
+ "shell injection"
98
+ end
99
+
100
+ def kind
101
+ "shell_injection"
102
+ end
103
+
104
+ def metadata
105
+ {
106
+ command: @command
107
+ }
108
+ end
109
+
110
+ def exception(*)
111
+ ShellInjectionError.new(self)
112
+ end
113
+ end
114
+
115
+ class SQLInjectionAttack < Attack
116
+ attr_reader :query
117
+ attr_reader :input
118
+ attr_reader :dialect
119
+
120
+ def initialize(query:, input:, dialect:, **opts)
121
+ super(**opts)
122
+ @query = query
123
+ @input = input
124
+ @dialect = dialect
125
+ end
126
+
127
+ def humanized_name
128
+ "SQL injection"
129
+ end
130
+
131
+ def kind
132
+ "sql_injection"
133
+ end
134
+
135
+ def metadata
136
+ {sql: @query}
137
+ end
138
+
139
+ def exception(*)
140
+ SQLInjectionError.new(self)
141
+ end
142
+ end
143
+
144
+ class SSRFAttack < Attack
145
+ attr_reader :input
146
+ attr_reader :request
147
+
148
+ def initialize(request:, input:, **opts)
149
+ super(**opts)
150
+ @input = input
151
+ @request = request
152
+ end
153
+
154
+ def humanized_name
155
+ "server-side request forgery"
156
+ end
157
+
158
+ def kind
159
+ "ssrf"
160
+ end
161
+
162
+ def exception(*)
163
+ SSRFDetectedError.new(self)
164
+ end
165
+
166
+ def metadata
167
+ {
168
+ host: @request.uri.hostname,
169
+ port: @request.uri.port
170
+ }
171
+ end
172
+ end
173
+
174
+ # Special case of an SSRF attack where we don't have a context—we're just
175
+ # detecting a request to a particularly sensitive address.
176
+ class StoredSSRFAttack < Attack
177
+ attr_reader :hostname
178
+ attr_reader :address
179
+
180
+ def initialize(hostname:, address:, **opts)
181
+ super(**opts)
182
+ @hostname = hostname
183
+ @address = address
184
+ end
185
+
186
+ def humanized_name
187
+ "server-side request forgery"
188
+ end
189
+
190
+ def exception(*)
191
+ SSRFDetectedError.new(self)
192
+ end
193
+
194
+ def kind
195
+ "ssrf"
196
+ end
197
+
198
+ def input
199
+ Aikido::Zen::Payload::UNKNOWN_PAYLOAD
200
+ end
201
+
202
+ def metadata
203
+ {}
204
+ end
205
+ end
206
+ end
207
+ end