logblock 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2bcf5fb5f81e733c5dc8c501ede3c95bf16bd4f91a8b0f91906921236238fa0d
4
+ data.tar.gz: 0bfa914c4b5ff89a7639732cf8139b131a26725ae375d09d91a9374e11413618
5
+ SHA512:
6
+ metadata.gz: 711a2a59ae90c254ecee597f19ba70ef232d1d6ae38fa52438923b5020bb6590de10665f0a084a0f7f5d66df3eb60e5df0e1e822b697e004c331eb5440d9f338
7
+ data.tar.gz: 310b9fb1cea2467851930835fdea7138e36ac7034191bbc1e51b2904d0e2bd7d30a1f8bf4d0aec53bc0d65ab800a298c46355af8b47e01ffc1f4d006e8b7656f
data/bin/logblock ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'ip'
5
+ require 'log_entries'
6
+
7
+ DEFAULT_OUTPUT_FILE = 'blocklist.txt'
8
+ DEFAULT_LOG_FILE = '/var/log/nginx/access.log'
9
+ FLAGS = { log_file: ["-l", "--log-file=LOG_FILE", "Path to access log"],
10
+ output_file: ["-o", "--output-file=OUTPUT_FILE", "Path for blocklist"] }
11
+
12
+ options = {}
13
+ pad = ->(s) { "\n#{s}\n\n" }
14
+
15
+ OptionParser.new { |opts|
16
+ opts.banner = pad.("Usage: #{$0} [options]")
17
+
18
+ opts.on("-h", "--help", "Show basic usage / options") do
19
+ puts "#{opts}\n"
20
+ exit
21
+ end
22
+
23
+ FLAGS.each { |key, arr| opts.on(*arr) { |v| options[key] = v } }
24
+
25
+ }.parse!
26
+
27
+ log_file = options[:log_file] || DEFAULT_LOG_FILE
28
+ output_file = options[:output_file] || DEFAULT_OUTPUT_FILE
29
+
30
+ if File.file?(log_file)
31
+ LogEntries.new(log_file, output_file).create_blocklist
32
+ puts pad.("blocklist successfully written: #{output_file}")
33
+ else
34
+ msg = "nginx access log doesn't exist: #{log_file}\n\n"
35
+ msg << "please run the following for usage: #{$0} -h"
36
+ puts pad.(msg)
37
+ end
data/lib/ip.rb ADDED
@@ -0,0 +1,40 @@
1
+ class IP
2
+ # -----------------------------------------------------------------------------
3
+ # https://www.cloudflare.com/ips-v4
4
+ EXPECTED_CIDRS = %w(
5
+ 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 131.0.72.0/22 197.234.240.0/22
6
+ 173.245.48.0/20 188.114.96.0/20 190.93.240.0/20 108.162.192.0/18 141.101.64.0/18
7
+ 198.41.128.0/17 162.158.0.0/15 172.64.0.0/13 104.16.0.0/12
8
+ )
9
+ BITS_IN_IP, BITS_IN_SEGMENT = 32, 8
10
+ attr_reader :bits
11
+
12
+ # accepts plain IP address, or CIDR notation
13
+ def initialize(cidr_str)
14
+ @ip, bits = cidr_str.split('/')
15
+ @bits = bits.to_i if bits
16
+ end
17
+
18
+ def to_s; @ip; end
19
+
20
+ # 192.168.0.1 -> 11000000101010000000000000000001
21
+ def to_binary
22
+ n_to_b = ->(n) { n.to_s(2).rjust(BITS_IN_SEGMENT, '0') }
23
+ @binary_ip ||= @ip.split('.').map { |segment| n_to_b.(segment.to_i) }.join
24
+ end
25
+
26
+ def sketch?
27
+ @sketch ||= IP.expected_cidrs.none? { |cidr| cidr.includes?(self) }
28
+ end
29
+
30
+ # IP.new('192.168.0.1/24').includes?(IP.new('192.168.0.1'))
31
+ def includes?(ip_addr)
32
+ len = self.bits
33
+ ip_slice = ->(ip) { ip.to_binary[0,len] }
34
+ ip_slice.(self) == ip_slice.(ip_addr)
35
+ end
36
+
37
+ def self.expected_cidrs
38
+ @cf_ips ||= EXPECTED_CIDRS.map { |cidr| IP.new(cidr) }
39
+ end
40
+ end
@@ -0,0 +1,62 @@
1
+ require 'log_entry'
2
+
3
+ class LogEntries
4
+ # number of permissable redirects
5
+ REDIRECT_MAX = 5
6
+ # span of time to check redirects
7
+ REDIRECT_WINDOW = 5
8
+
9
+ attr_reader :entries
10
+ def initialize(log_file, output_file)
11
+ @log_file, @output_file = log_file, output_file
12
+ @entries = {}
13
+ end
14
+
15
+ def add(log_entry)
16
+ @entries[log_entry.ip.to_s] ||= []
17
+ @entries[log_entry.ip.to_s] << log_entry
18
+ end
19
+
20
+ def sketch?(ip)
21
+ return false unless entries[ip.to_s]
22
+ redirects = entries[ip.to_s].select{ |entry| entry.redirect? }
23
+
24
+ ip.sketch? &&
25
+ redirects.count > REDIRECT_MAX &&
26
+ exceeds_window(redirects.map(&:time))
27
+ end
28
+
29
+ def sketch_ips
30
+ File.open(@log_file, 'r').each_line do |line|
31
+ if(entry = LogEntry.parse(line))
32
+ self.add(LogEntry.new(entry))
33
+ end
34
+ end
35
+
36
+ sketchy = []
37
+ self.entries.values.map(&:first).each do |entry|
38
+ sketchy << entry.ip.to_s if self.sketch?(entry.ip)
39
+ end
40
+ sketchy
41
+ end
42
+
43
+ def exceeds_window(time_arr)
44
+ return false if time_arr.count < REDIRECT_MAX
45
+ return true if (time_arr[REDIRECT_MAX - 1] - time_arr.first) < REDIRECT_WINDOW
46
+ exceeds_window(time_arr[1..-1])
47
+ end
48
+
49
+ def create_blocklist
50
+ new_blocks = sketch_ips
51
+ curr_blocks = []
52
+
53
+ if(File.file?(@output_file))
54
+ curr_blocks += File.read(@output_file).split("\n")
55
+ end
56
+
57
+ results = (curr_blocks + new_blocks).uniq.
58
+ reject(&:empty?).
59
+ sort_by { |ip| IP.new(ip).to_binary }
60
+ File.write(@output_file, results.join("\n"))
61
+ end
62
+ end
data/lib/log_entry.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'time'
2
+ require 'ip'
3
+
4
+ class LogEntry
5
+ TIME_FORMAT = '%d/%b/%Y:%H:%M:%S'
6
+ attr_reader :ip, :resp_code, :time
7
+
8
+ def initialize(attrs)
9
+ @ip, @resp_code, @time = attrs[:ip], attrs[:resp_code], attrs[:time]
10
+ end
11
+
12
+ # Format here is definitely for nginx logs, but easily adaptable
13
+ # 192.168.0.128 - - [25/Jan/2019:06:57:32 +0000] "GET / HTTP/1.1" 200 708 "-" "Awsome Browser"
14
+ def self.parse(line)
15
+ ip_r = '(\d+\.\d+\.\d+\.\d+)'
16
+ date_r = '\[(.*?)\]'
17
+ str_r = '"(.*?)"'
18
+ num_r = '(\d+)'
19
+ regex = /#{ip_r} - - #{date_r} #{str_r} #{num_r} #{num_r} #{str_r} #{str_r}/
20
+
21
+ if(line =~ regex)
22
+ ip, time, req, resp_code, bytes, _, agent = IP.new($1), $2, $3, $4, $5, $6, $7
23
+ time = Time.strptime(time, TIME_FORMAT)
24
+
25
+ { ip: ip, resp_code: resp_code, time: time }
26
+ end
27
+ end
28
+
29
+ def redirect?
30
+ @resp_code == '301'
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logblock
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Christopher Kuttruff
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-02-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.0.1
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.0.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 12.3.2
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 12.3.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 5.11.3
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 5.11.3
55
+ description:
56
+ email:
57
+ executables:
58
+ - logblock
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - bin/logblock
63
+ - lib/ip.rb
64
+ - lib/log_entries.rb
65
+ - lib/log_entry.rb
66
+ homepage: https://gitlab.com/slackz/log-block
67
+ licenses:
68
+ - BSD-3-Clause
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 3.0.1
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: simple ruby tool for identify sketchy nginx requests
89
+ test_files: []