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.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/Guardfile +4 -4
- data/README.md +37 -3
- data/conf/spanx-config.yml.example +5 -0
- data/lib/spanx.rb +1 -0
- data/lib/spanx/actor/analyzer.rb +1 -0
- data/lib/spanx/actor/log_reader.rb +18 -17
- data/lib/spanx/api.rb +7 -0
- data/lib/spanx/api/machine.rb +20 -0
- data/lib/spanx/api/resources/blocked_ips.rb +15 -0
- data/lib/spanx/api/resources/unblock_ip.rb +17 -0
- data/lib/spanx/cli.rb +12 -7
- data/lib/spanx/cli/analyze.rb +7 -6
- data/lib/spanx/cli/api.rb +70 -0
- data/lib/spanx/cli/disable.rb +2 -1
- data/lib/spanx/cli/enable.rb +2 -1
- data/lib/spanx/cli/flush.rb +27 -2
- data/lib/spanx/cli/report.rb +77 -0
- data/lib/spanx/cli/watch.rb +18 -11
- data/lib/spanx/helper/exit.rb +6 -2
- data/lib/spanx/helper/subclassing.rb +9 -0
- data/lib/spanx/logger.rb +1 -1
- data/lib/spanx/notifier/slack.rb +42 -0
- data/lib/spanx/runner.rb +1 -1
- data/lib/spanx/usage.rb +11 -7
- data/lib/spanx/version.rb +1 -1
- data/spanx.gemspec +3 -9
- data/spec/spanx/actor/analyzer_spec.rb +5 -5
- data/spec/spanx/actor/log_reader_spec.rb +78 -41
- data/spec/spanx/api/machine_spec.rb +33 -0
- data/spec/spanx/cli/cli_spec.rb +22 -0
- data/spec/spanx/config_spec.rb +2 -2
- data/spec/spanx/notifier/email_spec.rb +1 -1
- data/spec/spanx/notifier/slack_spec.rb +77 -0
- data/spec/spanx/runner_spec.rb +33 -33
- data/spec/spanx/usage_spec.rb +13 -0
- data/spec/spanx/whitelist_spec.rb +24 -24
- data/spec/spec_helper.rb +1 -0
- metadata +46 -111
- data/.pairs +0 -13
@@ -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
|
data/lib/spanx/cli/watch.rb
CHANGED
@@ -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 =>
|
15
|
-
:long =>
|
16
|
-
:description =>
|
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 =>
|
46
|
-
:long =>
|
47
|
-
:description =>
|
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 =>
|
68
|
-
:long =>
|
69
|
-
:description =>
|
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(
|
88
|
-
error_exit_with_msg(
|
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
|
data/lib/spanx/helper/exit.rb
CHANGED
@@ -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::
|
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
|
data/lib/spanx/logger.rb
CHANGED
@@ -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
|
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
|
data/lib/spanx/runner.rb
CHANGED
data/lib/spanx/usage.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
module Spanx
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
data/lib/spanx/version.rb
CHANGED
data/spanx.gemspec
CHANGED
@@ -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'
|
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'
|
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 =
|
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(
|
80
|
-
Spanx::IPChecker.should_receive(:new).with(ip2).and_return(
|
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) {
|
92
|
-
let(:blocked_ip) {
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
25
|
-
|
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
|
-
|
41
|
+
context 'if a line matches a whitelist' do
|
42
|
+
let(:whitelist) { double }
|
29
43
|
|
30
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
65
|
+
reader_thread.join
|
66
|
+
}.to yield_successive_args('9.9.9.10')
|
67
|
+
end
|
42
68
|
end
|
69
|
+
end
|
43
70
|
|
44
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
77
|
+
after do
|
78
|
+
file1.close
|
79
|
+
file2.close
|
80
|
+
end
|
50
81
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|