diff --git a/README.md b/README.md index 7c9d1f92..414d6a82 100644 --- a/README.md +++ b/README.md @@ -325,6 +325,34 @@ The API token to allow access to your Campfire account. For more options to set Campfire, like _ssl_, check [here](https://github.com/collectiveidea/tinder/blob/master/lib/tinder/campfire.rb#L17). +### Fail2ban notifier + +This notifier creates a log file you can then parse with [fail2ban](http://www.fail2ban.org/) + +#### Usage + +To configure the notifier, you don't need anything, although you can customise the `logifile` value, like this: + +```ruby +Whatever::Application.config.middleware.use ExceptionNotification::Rack, + :email => { + :email_prefix => "[Whatever] ", + :sender_address => %{"notifier" }, + :exception_recipients => %w{exceptions@example.com} + }, + :fail2ban => { + :logfile => '/path/to/logs/fail2ban.log' + } +``` + +#### Options + +##### logfile + +*String, not required* + +An alternative log file location. By default Rails.root.join('log', 'fail2ban.log') + ### HipChat notifier diff --git a/lib/exception_notifier/fail2ban_notifer.rb b/lib/exception_notifier/fail2ban_notifer.rb new file mode 100644 index 00000000..f1e6a922 --- /dev/null +++ b/lib/exception_notifier/fail2ban_notifer.rb @@ -0,0 +1,52 @@ +# This notifier outputs exception details in a log file that you can parse with +# fail2ban to detect possible attacks.. +# +# Fail2ban jail configuration (append to /etc/fail2ban/jail.local): +# +# [rails-app] +# enabled = true +# port = http,https +# filter = rails-app +# logpath = /path/to/app/log/fail2ban.log +# bantime = 3600 +# findtime = 600 +# maxretry = 10 +# +# +# Fail2ban filter configuration (save in /etc/fail2ban/filters.d/rails-app.conf): +# [Definition] +# failregex = : : +# ignoreregex = +# +require 'action_dispatch' + +module ExceptionNotifier + class Fail2banNotifier + + # This notifier only accepts a :logfile option that should point to a valid + # file that will be used to log exception entries. Point fail2ban to this + # file + def initialize(options) + @default_options = options + @default_options[:logfile] ||= Rails.root.join('log', 'fail2ban.log') + + # Roll over every 30M, keep 10 files + @logger ||= Logger.new(@default_options[:logfile], 10, 30*1024*1024) + end + + def call(exception, options={}) + env = options[:env] + request = ActionDispatch::Request.new(env) + + # : : -- + msg = "%s : %s : %s %s -- %s" % [ + request.remote_ip, + exception.class, + request.request_method, + env["PATH_INFO"], + request.filtered_parameters.inspect + ] + @logger.error(msg) + end + end +end \ No newline at end of file diff --git a/test/exception_notifier/fail2ban_notifier_test.rb b/test/exception_notifier/fail2ban_notifier_test.rb new file mode 100644 index 00000000..3174b3a6 --- /dev/null +++ b/test/exception_notifier/fail2ban_notifier_test.rb @@ -0,0 +1,58 @@ +require 'test_helper' +require 'httparty' + +class Fail2banNotifierTest < ActiveSupport::TestCase + test "should write exception notification to fail2ban log" do + custom_log = Rails.root.join('tmp/fail2ban.log') + + ExceptionNotifier::Fail2banNotifier.stubs(:new).returns(Object.new) + fail2ban = ExceptionNotifier::Fail2banNotifier.new({:logfile => custom_log}) + fail2ban.stubs(:call).returns(fake_response) + + notif = fail2ban.call(fake_exception) + + assert File.exists?(custom_log) + last_line = File.read_lines(custom_log).last + + # Very basic test that just verifies we've got a remote IP and an + # exception class in no particular order + assert last_line.include? fake_response[:body][:request][:ip_address] + assert last_line.include? fake_response[:body][:exception][:error_class] + end + + private + + def fake_response + { + :status => 200, + :body => { + :exception => { + :error_class => 'ZeroDivisionError', + :message => 'divided by 0', + :backtrace => '/exception_notification/test/webhook_notifier_test.rb:48:in `/' + }, + :data => { + :extra_data => {:data_item1 => "datavalue1", :data_item2 => "datavalue2"} + }, + :request => { + :cookies => {:cookie_item1 => 'cookieitemvalue1', :cookie_item2 => 'cookieitemvalue2'}, + :url => 'http://example.com/example', + :ip_address => '192.168.1.1', + :environment => {:env_item1 => "envitem1", :env_item2 => "envitem2"}, + :controller => '#', + :session => {:session_item1 => "sessionitem1", :session_item2 => "sessionitem2"}, + :parameters => {:action =>"index", :controller =>"projects"} + } + } + } + end + + def fake_exception + exception = begin + 5/0 + rescue Exception => e + e + end + end + +end