Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/qa-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ jobs:
cat Dockerfile

- name: Run Firewall QA Tests
uses: AikidoSec/firewall-tester-action@v1.0.9
uses: AikidoSec/firewall-tester-action@v1.0.11
with:
dockerfile_path: ./zen-demo-ruby/Dockerfile
app_port: 3000
sleep_before_test: 30
extra_args: "-e RAILS_ENV=test -e AIKIDO_CLIENT_IP_HEADER=HTTP_X_FORWARDED_FOR"
max_parallel_tests: 15
skip_tests: test_outbound_domain_blocking,test_rate_limiting_group_id_1_minute,test_user_rate_limiting_1_minute_enable_disable
skip_tests: test_rate_limiting_group_id_1_minute,test_user_rate_limiting_1_minute_enable_disable
14 changes: 13 additions & 1 deletion lib/aikido/zen.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
require_relative "zen/middleware/attack_wave_protector"
require_relative "zen/middleware/request_tracker"
require_relative "zen/outbound_connection"
require_relative "zen/outbound_connection_monitor"
require_relative "zen/runtime_settings"
require_relative "zen/rate_limiter"
require_relative "zen/attack_wave"
Expand Down Expand Up @@ -213,6 +212,19 @@ class << self
alias_method :set_user, :track_user
end

def self.block_outbound?(connection)
context = current_context
settings = runtime_settings

unless context.nil?
request = context.request

return false if settings.bypassed_ips.include?(request.client_ip)
end

settings.block_outbound?(connection)
end

# Marks that the Zen middleware was installed properly
# @return void
def self.middleware_installed!
Expand Down
2 changes: 1 addition & 1 deletion lib/aikido/zen/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class Config
# the oldest seen users.
attr_accessor :max_users_tracked

# @return [Proc{(Aikido::Zen::Request, Symbol) => Array(Integer, Hash, #each)}]
# @return [Proc{(Aikido::Zen::Request, Symbol, reason: String=nil) => Array(Integer, Hash, #each)}]
# Rack handler used to respond to requests from IPs, users or others blocked in the Aikido
# dashboard.
attr_accessor :blocked_responder
Expand Down
6 changes: 6 additions & 0 deletions lib/aikido/zen/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,11 @@ def initialize(msg)
super
end
end

class OutboundConnectionBlockedError < StandardError
def initialize(connection)
super("Zen blocked an outbound connection to #{connection.host}.")
end
end
end
end
23 changes: 0 additions & 23 deletions lib/aikido/zen/outbound_connection_monitor.rb

This file was deleted.

19 changes: 18 additions & 1 deletion lib/aikido/zen/runtime_settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ module Aikido::Zen
#
# You can subscribe to changes with +#add_observer(object, func_name)+, which
# will call the function passing the settings as an argument
RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :bypassed_ips, :received_any_stats, :blocking_mode, :blocked_user_agent_regexp, :monitored_user_agent_regexp, :user_agent_details, :blocked_ip_lists, :allowed_ip_lists, :monitored_ip_lists) do
RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :bypassed_ips, :received_any_stats, :blocking_mode, :blocked_user_agent_regexp, :monitored_user_agent_regexp, :user_agent_details, :blocked_ip_lists, :allowed_ip_lists, :monitored_ip_lists, :block_new, :domains) do
def initialize(*)
super
self.endpoints ||= RuntimeSettings::Endpoints.new
self.bypassed_ips ||= RuntimeSettings::IPSet.new
self.blocked_ip_lists ||= []
self.allowed_ip_lists ||= []
self.monitored_ip_lists ||= []
self.domains ||= []
end

# @!attribute [rw] updated_at
Expand Down Expand Up @@ -62,6 +63,12 @@ def initialize(*)
# @!attribute [rw] user_agent_details
# @return [Regexp]

# @!attribute [rw] block_new
# @return [Boolean]

# @!attribute [rw] domains
# @return [Array<Aikido::Zen::RuntimeSettings::DomainSettings>]

# Parse and interpret the JSON response from the core API with updated
# runtime settings, and apply the changes.
#
Expand All @@ -81,6 +88,9 @@ def update_from_runtime_config_json(data)
self.received_any_stats = data["receivedAnyStats"]
self.blocking_mode = data["block"]

self.block_new = data["blockNewOutgoingRequests"]
self.domains = RuntimeSettings::Domains.from_json(data["domains"])

updated_at != last_updated_at
end

Expand Down Expand Up @@ -186,9 +196,16 @@ def monitored_ip_list_keys(ip)

monitored_ip_lists.filter_map { |ip_list| ip_list.key if ip_list.include?(ip) }
end

def block_outbound?(connection)
return true if domains.include?(connection.host) && domains[connection.host].block?

block_new && domains[connection.host].block?
end
end
end

require_relative "runtime_settings/ip_set"
require_relative "runtime_settings/ip_list"
require_relative "runtime_settings/endpoints"
require_relative "runtime_settings/domains"
31 changes: 31 additions & 0 deletions lib/aikido/zen/runtime_settings/domain_settings.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Aikido::Zen
class RuntimeSettings::DomainSettings
def self.none
@no_settings ||= new(mode: :block)
end

def self.from_json(data)
new(
mode: data["mode"]&.to_sym
)
end

attr_reader :mode

def initialize(mode:)
raise ArgumentError, "mode must be either :block or :allow" unless [:block, :allow].include?(mode)

@mode = mode
end

def block?
@mode == :block
end

def allow?
@mode == :allow
end
end
end
34 changes: 34 additions & 0 deletions lib/aikido/zen/runtime_settings/domains.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require_relative "domain_settings"

module Aikido::Zen
class RuntimeSettings::Domains
def self.from_json(data)
domain_pairs = Array(data).map do |value|
hostname = value["hostname"].downcase
settings = RuntimeSettings::DomainSettings.from_json(value)
[hostname, settings]
end

new(domain_pairs.to_h)
end

def initialize(domains = {})
@domains = domains
@domains.default = RuntimeSettings::DomainSettings.none
end

def [](hostname)
@domains[hostname.downcase]
end

def include?(hostname)
@domains.key?(hostname.downcase)
end

def size
@domains.size
end
end
end
5 changes: 5 additions & 0 deletions lib/aikido/zen/sinks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
require_relative "sinks/file"
require_relative "sinks/socket"
require_relative "sinks/resolv"

# HTTP clients

require_relative "sinks/net_http"

# http.rb aims to support and is tested against Ruby 3.0+:
Expand All @@ -29,6 +32,8 @@
require_relative "sinks/async_http"
require_relative "sinks/em_http"

# Database drivers

require_relative "sinks/mysql2"
require_relative "sinks/pg"
require_relative "sinks/sqlite3"
Expand Down
12 changes: 9 additions & 3 deletions lib/aikido/zen/sinks/async_http.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# frozen_string_literal: true

require_relative "../scanners/ssrf_scanner"
require_relative "../outbound_connection_monitor"

module Aikido::Zen
module Sinks
module Async
module HTTP
SINK = Sinks.add("async-http", scanners: [
Scanners::SSRFScanner,
OutboundConnectionMonitor
Scanners::SSRFScanner
])

module Helpers
Expand Down Expand Up @@ -52,8 +50,16 @@ def self.load_sinks!

connection = OutboundConnection.from_uri(uri)

if Aikido::Zen.block_outbound?(connection)
Sinks::DSL.presafe do
raise OutboundConnectionBlockedError.new(connection)
end
end

Helpers.scan(wrapped_request, connection, "request")

Aikido::Zen.track_outbound(connection)

response = original_call.call

Scanners::SSRFScanner.track_redirects(
Expand Down
12 changes: 9 additions & 3 deletions lib/aikido/zen/sinks/curb.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
# frozen_string_literal: true

require_relative "../scanners/ssrf_scanner"
require_relative "../outbound_connection_monitor"

module Aikido::Zen
module Sinks
module Curl
SINK = Sinks.add("curb", scanners: [
Scanners::SSRFScanner,
OutboundConnectionMonitor
Scanners::SSRFScanner
])

module Helpers
Expand Down Expand Up @@ -64,8 +62,16 @@ def self.load_sinks!

connection = OutboundConnection.from_uri(URI(url))

if Aikido::Zen.block_outbound?(connection)
Sinks::DSL.presafe do
raise OutboundConnectionBlockedError.new(connection)
end
end

Helpers.scan(wrapped_request, connection, "request")

Aikido::Zen.track_outbound(connection)

response = original_call.call

Scanners::SSRFScanner.track_redirects(
Expand Down
12 changes: 9 additions & 3 deletions lib/aikido/zen/sinks/em_http.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# frozen_string_literal: true

require_relative "../scanners/ssrf_scanner"
require_relative "../outbound_connection_monitor"

module Aikido::Zen
module Sinks
module EventMachine
module HttpRequest
SINK = Sinks.add("em-http-request", scanners: [
Scanners::SSRFScanner,
OutboundConnectionMonitor
Scanners::SSRFScanner
])

module Helpers
Expand Down Expand Up @@ -50,7 +48,15 @@ def self.load_sinks!
port: req.port
)

if Aikido::Zen.block_outbound?(connection)
Sinks::DSL.presafe do
raise OutboundConnectionBlockedError.new(connection)
end
end

Helpers.scan(wrapped_request, connection, "request")

Aikido::Zen.track_outbound(connection)
end
end
end
Expand Down
12 changes: 9 additions & 3 deletions lib/aikido/zen/sinks/excon.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
# frozen_string_literal: true

require_relative "../scanners/ssrf_scanner"
require_relative "../outbound_connection_monitor"

module Aikido::Zen
module Sinks
module Excon
SINK = Sinks.add("excon", scanners: [
Scanners::SSRFScanner,
OutboundConnectionMonitor
Scanners::SSRFScanner
])

module Helpers
Expand Down Expand Up @@ -56,8 +54,16 @@ def self.load_sinks!

connection = OutboundConnection.from_uri(request.uri)

if Aikido::Zen.block_outbound?(connection)
Sinks::DSL.presafe do
raise OutboundConnectionBlockedError.new(connection)
end
end

Helpers.scan(request, connection, "request")

Aikido::Zen.track_outbound(connection)

response = original_call.call

Scanners::SSRFScanner.track_redirects(
Expand Down
12 changes: 9 additions & 3 deletions lib/aikido/zen/sinks/http.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
# frozen_string_literal: true

require_relative "../scanners/ssrf_scanner"
require_relative "../outbound_connection_monitor"

module Aikido::Zen
module Sinks
module HTTP
SINK = Sinks.add("http", scanners: [
Scanners::SSRFScanner,
OutboundConnectionMonitor
Scanners::SSRFScanner
])

module Helpers
Expand Down Expand Up @@ -70,8 +68,16 @@ def self.load_sinks!

connection = Helpers.build_outbound(req)

if Aikido::Zen.block_outbound?(connection)
Sinks::DSL.presafe do
raise OutboundConnectionBlockedError.new(connection)
end
end

Helpers.scan(wrapped_request, connection, "request")

Aikido::Zen.track_outbound(connection)

response = original_call.call

Scanners::SSRFScanner.track_redirects(
Expand Down
Loading
Loading