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 +7 -0
- data/bin/logblock +37 -0
- data/lib/ip.rb +40 -0
- data/lib/log_entries.rb +62 -0
- data/lib/log_entry.rb +32 -0
- metadata +89 -0
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
|
data/lib/log_entries.rb
ADDED
|
@@ -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: []
|