spanx 0.1.1 → 0.3.0

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.
@@ -0,0 +1,77 @@
1
+ require 'mixlib/cli'
2
+ require 'spanx/logger'
3
+
4
+ class Spanx::CLI::Report < Spanx::CLI
5
+
6
+ banner 'Usage: spanx report [ -b | -t ] [options]'
7
+ description 'Report on tracked and/or blocked IPs'
8
+
9
+ option :blocked,
10
+ :short => '-b',
11
+ :long => '--blocked',
12
+ :description => 'Show all currently blocked IPs',
13
+ :required => false
14
+
15
+ option :tracked,
16
+ :short => '-t',
17
+ :long => '--tracked',
18
+ :description => 'Show all IPs seen within the tracked period',
19
+ :required => false
20
+
21
+ option :summary,
22
+ :short => '-s',
23
+ :long => '--summary',
24
+ :description => 'Only show summary',
25
+ :required => false
26
+
27
+ option :config_file,
28
+ :short => '-c CONFIG',
29
+ :long => '--config CONFIG',
30
+ :description => 'Path to config file (YML)',
31
+ :required => true
32
+
33
+ option :debug,
34
+ :short => '-g',
35
+ :long => '--debug',
36
+ :description => 'Log to STDOUT status of execution and some time metrics',
37
+ :boolean => true,
38
+ :required => false,
39
+ :default => false
40
+
41
+ option :help,
42
+ :short => '-h',
43
+ :long => '--help',
44
+ :description => 'Show this message',
45
+ :on => :tail,
46
+ :boolean => true,
47
+ :show_options => true,
48
+ :exit => 0
49
+
50
+
51
+ def run(argv = ARGV)
52
+ generate_config(argv)
53
+ out = ''
54
+ if config[:blocked]
55
+ ips = Spanx::IPChecker.rate_limited_identifiers
56
+ out << report_ips('Blocked', ips)
57
+ end
58
+ if config[:tracked]
59
+ ips = Spanx::IPChecker.tracked_identifiers
60
+ out << report_ips('Tracked', ips)
61
+ end
62
+ if config[:summary] or (config[:blocked].nil? && config[:tracked].nil?)
63
+ out << " Total tracked IPS: #{Spanx::IPChecker.tracked_identifiers.size}\n"
64
+ out << " Total blocked IPS: #{Spanx::IPChecker.rate_limited_identifiers.size}\n"
65
+ out << "___________________\n\n"
66
+ out << "Keeping history for: #{config[:collector][:history] / 3600 }hrs\n"
67
+ out << " Time resolution: #{config[:collector][:resolution] / 60 }min\n"
68
+ end
69
+ puts out
70
+ out
71
+ end
72
+
73
+ private
74
+ def report_ips name, ips = []
75
+ ips.empty? ? "No #{name.downcase} IPs were found.\n" : "#{name} IPs:\n" + ips.join("\n") + "\n"
76
+ end
77
+ end
@@ -10,10 +10,17 @@ class Spanx::CLI::Watch < Spanx::CLI
10
10
  Usage: spanx watch [options]
11
11
  EOF
12
12
 
13
+ description 'Watch a server log file and write out a block list file'
14
+
13
15
  option :access_log,
14
- :short => "-f ACCESS_LOG",
15
- :long => "--file ACCESS_LOG",
16
- :description => "Apache/nginx access log file to scan continuously",
16
+ :short => '-f ACCESS_LOG',
17
+ :long => '--file ACCESS_LOG',
18
+ :description => 'Apache/nginx access log file to scan continuously. Can be set multiple times.',
19
+ :proc => ->(f) {
20
+ @watched_log_files ||= []
21
+ @watched_log_files << f
22
+ @watched_log_files.uniq!
23
+ },
17
24
  :required => false
18
25
 
19
26
  option :config_file,
@@ -42,9 +49,9 @@ class Spanx::CLI::Watch < Spanx::CLI
42
49
  :required => false
43
50
 
44
51
  option :daemonize,
45
- :short => "-d",
46
- :long => "--daemonize",
47
- :description => "Detach from TTY and run as a daemon",
52
+ :short => '-d',
53
+ :long => '--daemonize',
54
+ :description => 'Detach from TTY and run as a daemon',
48
55
  :boolean => true,
49
56
  :default => false
50
57
 
@@ -64,9 +71,9 @@ class Spanx::CLI::Watch < Spanx::CLI
64
71
  :default => false
65
72
 
66
73
  option :help,
67
- :short => "-h",
68
- :long => "--help",
69
- :description => "Show this message",
74
+ :short => '-h',
75
+ :long => '--help',
76
+ :description => 'Show this message',
70
77
  :on => :tail,
71
78
  :boolean => true,
72
79
  :show_options => true,
@@ -84,8 +91,8 @@ class Spanx::CLI::Watch < Spanx::CLI
84
91
  private
85
92
 
86
93
  def validate!
87
- error_exit_with_msg("Could not find file. Use -f or set :file in config_file") unless config[:access_log] && File.exists?(config[:access_log])
88
- error_exit_with_msg("-b block_file is required") unless config[:block_file]
94
+ error_exit_with_msg('Could not find file. Use -f or set :file in config_file') unless config[:access_log] && File.exist?(config[:access_log].first)
95
+ error_exit_with_msg('-b block_file is required') unless config[:block_file]
89
96
  end
90
97
 
91
98
  end
@@ -2,10 +2,14 @@ module Spanx
2
2
  module Helper
3
3
  module Exit
4
4
  def error_exit_with_msg(msg)
5
- $stderr.puts "Error: #{msg}"
6
- $stderr.puts Spanx::USAGE
5
+ $stderr.puts "Error: #{msg}\n"
6
+ $stderr.puts Spanx::Usage.usage
7
7
  exit 1
8
8
  end
9
+ def help_exit
10
+ $stdout.puts Spanx::Usage.usage
11
+ exit 0
12
+ end
9
13
  end
10
14
  end
11
15
  end
@@ -24,6 +24,15 @@ module Spanx
24
24
 
25
25
  def inherited(subclass)
26
26
  subclasses[subclass.subclass_name] = subclass
27
+ subclass.instance_eval do
28
+ @description = nil
29
+ class << self
30
+ def description value = nil
31
+ @description ||= value
32
+ @description
33
+ end
34
+ end
35
+ end
27
36
  end
28
37
  end
29
38
  end
@@ -38,7 +38,7 @@ module Spanx
38
38
  end
39
39
  log "(#{"%9.2f" % (1000 * elapsed_time)}ms) #{message}"
40
40
  returned_from_block
41
- rescue Exception => e
41
+ rescue StandardError => e
42
42
  elapsed_time = Time.now - start
43
43
  log "(#{"%9.2f" % (1000 * elapsed_time)}ms) error: #{e.message} for #{message} "
44
44
  end
@@ -0,0 +1,42 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+
5
+ module Spanx
6
+ module Notifier
7
+ class Slack < Base
8
+ attr_reader :config
9
+
10
+ def initialize(config)
11
+ @config = config[:slack]
12
+ end
13
+
14
+ def publish blocked_ip
15
+ return nil unless enabled?
16
+ message = generate_block_ip_message(blocked_ip)
17
+
18
+ uri = endpoint
19
+
20
+ http = Net::HTTP.new(uri.host, uri.port)
21
+ http.use_ssl = true
22
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
23
+
24
+ request = Net::HTTP::Post.new(uri.request_uri, {'Content-Type' => 'text/json'})
25
+ request.body = { text: message }.to_json
26
+
27
+ http.request(request)
28
+ end
29
+
30
+ def endpoint
31
+ return nil unless enabled?
32
+ token = config[:token]
33
+ base_url = config[:base_url]
34
+ URI.parse("#{base_url}/services/hooks/incoming-webhook?token=#{token}")
35
+ end
36
+
37
+ def enabled?
38
+ config && config[:enabled]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -68,7 +68,7 @@ module Spanx
68
68
  private
69
69
 
70
70
  def validate_args!(args)
71
- raise("Invalid actor") unless (args - %w[collector log_reader writer analyzer]).empty?
71
+ raise('Invalid actor') unless (args - %w[collector log_reader writer analyzer]).empty?
72
72
  end
73
73
  end
74
74
  end
@@ -1,9 +1,13 @@
1
1
  module Spanx
2
- USAGE = %q{Usage: spanx command [options]
3
- watch -- Watch a server log file and write out a block list file
4
- analyze -- Analyze IP traffic and save blocked IPs into Redis
5
- flush -- Remove all IP blocks and delete previous tracking of that IP
6
- disable -- Disable IP blocking
7
- enable -- Enable IP blocking if disabled
8
- }
2
+ class Usage
3
+ HEADER = %q{Usage: spanx [ --help | <command> ] [options]}
4
+
5
+ def self.usage
6
+ out = ''
7
+ out << HEADER + "\n\n"
8
+ Spanx::CLI.subclasses.each_pair{|command, clazz| out << "#{sprintf "%10s", command}\t#{clazz.description}\n"}
9
+ out << "\nRun 'spanx <command> --help' to see command-specific options.\n"
10
+ out
11
+ end
12
+ end
9
13
  end
@@ -1,3 +1,3 @@
1
1
  module Spanx
2
- VERSION = "0.1.1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -15,18 +15,12 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = %w(lib)
16
16
  gem.version = Spanx::VERSION
17
17
 
18
- gem.add_dependency 'pause', '~> 0.0.4'
18
+ gem.add_dependency 'pause'
19
19
  gem.add_dependency 'file-tail'
20
20
  gem.add_dependency 'mixlib-cli'
21
21
  gem.add_dependency 'daemons'
22
22
  gem.add_dependency 'tinder'
23
- gem.add_dependency 'mail', '~> 2.4.4'
24
-
25
- gem.add_development_dependency 'rspec'
26
- gem.add_development_dependency 'fakeredis'
27
- gem.add_development_dependency 'timecop'
28
-
29
- gem.add_development_dependency 'guard-rspec'
30
- gem.add_development_dependency 'rb-fsevent'
23
+ gem.add_dependency 'mail'
31
24
 
25
+ gem.add_dependency 'webmachine'
32
26
  end
@@ -6,7 +6,7 @@ describe Spanx::Actor::Analyzer do
6
6
  include Spanx::Helper::Timing
7
7
 
8
8
  before do
9
- pause_config = mock(resolution: 10, history: 100, redis_host: "1.2.3.4", redis_port: 1, redis_db: 1)
9
+ pause_config = double(resolution: 10, history: 100, redis_host: "1.2.3.4", redis_port: 1, redis_db: 1)
10
10
  Pause.stub(:config).and_return(pause_config)
11
11
  pause_analyzer = Pause::Analyzer.new
12
12
  Pause.stub(:analyzer).and_return(pause_analyzer)
@@ -76,8 +76,8 @@ describe Spanx::Actor::Analyzer do
76
76
 
77
77
  before do
78
78
  Spanx::IPChecker.should_receive(:tracked_identifiers).and_return([ip1, ip2])
79
- Spanx::IPChecker.should_receive(:new).with(ip1).and_return(mock(analyze: nil))
80
- Spanx::IPChecker.should_receive(:new).with(ip2).and_return(mock(analyze: nil))
79
+ Spanx::IPChecker.should_receive(:new).with(ip1).and_return(double(analyze: nil))
80
+ Spanx::IPChecker.should_receive(:new).with(ip2).and_return(double(analyze: nil))
81
81
  end
82
82
 
83
83
  it "analyzes each IP found" do
@@ -88,8 +88,8 @@ describe Spanx::Actor::Analyzer do
88
88
 
89
89
  context "notifiers" do
90
90
  let(:notifiers) { ["FakeNotifier"] }
91
- let(:fake_notifier) { mock() }
92
- let(:blocked_ip) { mock() }
91
+ let(:fake_notifier) { double() }
92
+ let(:blocked_ip) { double() }
93
93
 
94
94
  class FakeNotifier
95
95
  end
@@ -3,66 +3,103 @@ require 'file/tail'
3
3
  require 'timeout'
4
4
  require 'thread'
5
5
  require 'tempfile'
6
+ require 'timecop'
6
7
 
7
8
  describe Spanx::Actor::LogReader do
8
9
 
9
- def test_log_file(file, expected_ip_count, expected_line_count, whitelist = nil)
10
- counter = 0
11
- ip_hash = {}
12
- reader = Spanx::Actor::LogReader.new(file, Queue.new, 1, whitelist)
13
- reader.file.backward(1000)
14
-
15
- t_reader = Thread.new do
16
- begin
17
- timeout(read_timeout) do
18
- reader.read do |ip|
19
- counter += 1
20
- ip_hash[ip] ||= 0
21
- ip_hash[ip] += 1
10
+ subject(:reader) { Spanx::Actor::LogReader.new(files, queue, 1, whitelist) }
11
+ let(:files) { [] }
12
+ let(:queue) { [] }
13
+ let(:whitelist) { nil }
14
+ let(:read_timeout) { 0.1 }
15
+
16
+ describe '#read' do
17
+ let(:tempfile) { Tempfile.new('access.log') }
18
+ let!(:file) { Spanx::Actor::File.new(tempfile.path) }
19
+
20
+ it 'yields each ip in a file as it is written' do
21
+ expect { |b|
22
+ reader_thread = Thread.new do
23
+ begin
24
+ timeout(read_timeout) do
25
+ reader.read(file, &b)
26
+ end
27
+ rescue TimeoutError
22
28
  end
23
29
  end
24
- rescue TimeoutError
25
- end
30
+
31
+ ::File.open(tempfile.path, 'a') do |t|
32
+ t.puts '9.9.9.9 - some stuff'
33
+ t.puts '9.9.9.10 - some other stuff'
34
+ t.puts '9.9.9.9 - whoa moar stuff'
35
+ end
36
+
37
+ reader_thread.join
38
+ }.to yield_successive_args('9.9.9.9', '9.9.9.10', '9.9.9.9')
26
39
  end
27
40
 
28
- yield if block_given?
41
+ context 'if a line matches a whitelist' do
42
+ let(:whitelist) { double }
29
43
 
30
- t_reader.join
44
+ before do
45
+ allow(whitelist).to receive(:match?).with("9.9.9.9 - some stuff\n").and_return(true)
46
+ allow(whitelist).to receive(:match?).with("9.9.9.10 - some other stuff\n").and_return(false)
47
+ end
31
48
 
32
- counter.should eql(expected_line_count)
33
- ip_hash.keys.size.should eql(expected_ip_count)
34
- end
49
+ it 'skips that line' do
50
+ expect { |b|
51
+ reader_thread = Thread.new do
52
+ begin
53
+ timeout(read_timeout) do
54
+ reader.read(file, &b)
55
+ end
56
+ rescue TimeoutError
57
+ end
58
+ end
35
59
 
36
- let(:file_name) { "spec/fixtures/access.log.1" }
37
- let(:read_timeout) { 0.05 }
60
+ ::File.open(tempfile.path, 'a') do |t|
61
+ t.puts '9.9.9.9 - some stuff'
62
+ t.puts '9.9.9.10 - some other stuff'
63
+ end
38
64
 
39
- context "#read" do
40
- it "should be able to read and parse IPs from a static file" do
41
- test_log_file(file_name, 82, 104)
65
+ reader_thread.join
66
+ }.to yield_successive_args('9.9.9.10')
67
+ end
42
68
  end
69
+ end
43
70
 
44
- it "should be able to read and parse IPs from a file being appended to" do
45
- tempfile = Tempfile.new("access.log")
71
+ describe '#run' do
72
+ let!(:file1) { Tempfile.new('log.log') }
73
+ let!(:file2) { Tempfile.new('loge.log') }
74
+ let!(:files) { [file1.path, file2.path] }
75
+ let(:read_timeout) { 0.1 }
46
76
 
47
- contents = ::File.read(file_name)
48
- tempfile.write(contents)
49
- tempfile.close
77
+ after do
78
+ file1.close
79
+ file2.close
80
+ end
50
81
 
51
- test_log_file(tempfile.path, 83, 105) do
52
- t_log_appender = Thread.new do
53
- ::File.open(tempfile.path, "a") do |t|
54
- t.write("9.9.9.9 - content")
82
+ xit 'pushes ips from each watched file onto queue' do
83
+ Timecop.freeze(Time.at(1409270561))
84
+
85
+ runner = Thread.new do
86
+ begin
87
+ timeout(read_timeout) do
88
+ reader.run
89
+ reader.threads.last.join
55
90
  end
91
+ rescue TimeoutError
56
92
  end
57
- t_log_appender.join
58
93
  end
59
- end
60
- end
61
94
 
62
- context "#whitelist" do
63
- let(:whitelist_file) { "spec/fixtures/whitelist.txt" }
64
- it "should exclude googlebot log lines" do
65
- test_log_file("spec/fixtures/access.log.bots", 1, 1, Spanx::Whitelist.new(whitelist_file))
95
+ sleep 1
96
+
97
+ file1.puts '1.1.1.1 - everyone loves a log!'
98
+ file2.puts '2.2.2.2 - only some people love a loge'
99
+
100
+ runner.join
101
+
102
+ expect(queue).to match_array([['1.1.1.1', 1409270561], ['2.2.2.2', 1409270561]])
66
103
  end
67
104
  end
68
105
  end