unseen-ip-to-airbrake 1.0.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/identify-ip-protocol-port +28 -0
- data/bin/unseen-connections +81 -0
- data/lib/identify_util.rb +21 -0
- data/lib/identify_util_spec.rb +11 -0
- data/lib/unseen_connections_monitor.rb +130 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 786fc0139b6baa2c5c7474c05c7d9651507a3463
|
4
|
+
data.tar.gz: 6ef92639e0b8e54b2dab02b1189fc80cd0552fe5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e6a62401f054c7b23af8767d338d1f29af55dc0e2b0098f3c5781205c9b6346f32b9e8def4eff33db17e5f95f4af3b1a04ff67819175c88a8d8c75009cedf035
|
7
|
+
data.tar.gz: 3dbc0ff1ecaa2e17d87d1f2f8305bf8502e74d358a8090e046abe5d300021fcf6642cc9456f9a5c88481100c3cf4cb5063e324acfc618d76fdfa2ef0a18feda3
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# identify-ip-protocol-port: script to identify an ip port combination
|
4
|
+
#
|
5
|
+
# Usage:
|
6
|
+
#
|
7
|
+
# $ identify-ip-protocol-port 8.8.8.8 udp 53
|
8
|
+
# {
|
9
|
+
# "ip": "8.8.8.8",
|
10
|
+
# "protocol": "udp",
|
11
|
+
# "port": "53",
|
12
|
+
# "service_name": "domain",
|
13
|
+
# "host": "google-public-dns-a.google.com"
|
14
|
+
# }
|
15
|
+
#
|
16
|
+
require_relative '../lib/identify_util.rb'
|
17
|
+
require 'json'
|
18
|
+
|
19
|
+
def usage
|
20
|
+
puts File.read(__FILE__).scan(/^# (.*)$/)
|
21
|
+
exit 0
|
22
|
+
end
|
23
|
+
|
24
|
+
usage if ARGV.empty?
|
25
|
+
|
26
|
+
ip, protocol, port = *ARGV
|
27
|
+
puts JSON.pretty_generate(IdentifyUtil.ip_protocol_port(ip, protocol, port))
|
28
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# unseen-connections: script to notify about unseen connection attempts
|
4
|
+
#
|
5
|
+
# Usage:
|
6
|
+
#
|
7
|
+
# elktail -f '%@timestamp,%src_ip,%dst_ip,%protocol,%src_port' 'type:cisco-firewall AND action:Built AND direction:outbound' \
|
8
|
+
# | unseen-connections
|
9
|
+
#
|
10
|
+
# Synopsis:
|
11
|
+
#
|
12
|
+
# The script expects input on STDIN, one line per firewall log entry.
|
13
|
+
# Each line must be formatted, including the fields %@timestamp, %src_ip, %dst_ip, %protocol and %src_port.
|
14
|
+
#
|
15
|
+
# %@timestamp,%src_ip,%dst_ip,%protocol,%src_port\n
|
16
|
+
#
|
17
|
+
# e.g.
|
18
|
+
#
|
19
|
+
# 2016-11-29T10:36:05.990Z,50.31.164.146,10.248.177.12,TCP,443\n
|
20
|
+
#
|
21
|
+
# It remembers each destination ip:port combination for a given time.
|
22
|
+
# Over multiple runs it restores and saves its state with a YAML file.
|
23
|
+
#
|
24
|
+
# New ip:port combinations are reported to Airbrake with
|
25
|
+
# their respective environment and additional information.
|
26
|
+
#
|
27
|
+
# Airbrake in turn alerts the current Sherrif in charge via Pivotaltracker.
|
28
|
+
#
|
29
|
+
# Dependencies:
|
30
|
+
#
|
31
|
+
# * A tool that can stream data out of the ELK stack and format its output as described above.
|
32
|
+
# For example 'elktail' (https://github.com/knes1/elktail/releases) does the job really well.
|
33
|
+
#
|
34
|
+
# * An Airbrake account project_id and project_key.
|
35
|
+
#
|
36
|
+
# * A yaml configuration file with symbol keys, including:
|
37
|
+
#
|
38
|
+
# ---
|
39
|
+
# # all keys are symbols
|
40
|
+
# # 7 days in seconds
|
41
|
+
# :remember_for: 604800
|
42
|
+
# # 1 hour in seconds
|
43
|
+
# :save_interval: 3600
|
44
|
+
# # were to save state
|
45
|
+
# :state_file: path/to/state.yml
|
46
|
+
# :airbrake:
|
47
|
+
# :project_id: <your airbrake project id>
|
48
|
+
# :project_key: <your airbrake project key>
|
49
|
+
# :networks:
|
50
|
+
# # name with regular expression to match against ip
|
51
|
+
# :staging: !ruby/regexp /10.248.177/
|
52
|
+
# :production: !ruby/regexp /10.248.237/
|
53
|
+
require_relative '../lib/unseen_connections_monitor.rb'
|
54
|
+
require 'optparse'
|
55
|
+
|
56
|
+
def usage
|
57
|
+
puts File.read(__FILE__).scan(/^# (.*)$/)
|
58
|
+
exit 0
|
59
|
+
end
|
60
|
+
|
61
|
+
help = false
|
62
|
+
config_file = 'config.yml'
|
63
|
+
OptionParser.new do |options|
|
64
|
+
options.on('-cFILE', '--config=FILE', 'Set configuration file [config.yml]') do |value|
|
65
|
+
config_file = value
|
66
|
+
end
|
67
|
+
options.on('-h', '--help', 'Display help') do
|
68
|
+
usage
|
69
|
+
end
|
70
|
+
end.parse!
|
71
|
+
|
72
|
+
monitor = UnseenConnectionsMonitor.new(config: YAML.load_file(config_file))
|
73
|
+
|
74
|
+
at_exit { monitor.close }
|
75
|
+
|
76
|
+
while line = STDIN.gets
|
77
|
+
datetime, from_ip, to_ip, protocol, port = line.strip.split(",")
|
78
|
+
warning = monitor.feed(datetime: datetime, from_ip: from_ip, protocol: protocol, to_ip: to_ip, port: port)
|
79
|
+
STDERR.puts(warning.message) if warning
|
80
|
+
end
|
81
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'resolv'
|
3
|
+
|
4
|
+
module IdentifyUtil
|
5
|
+
def ip_protocol_port(ip, protocol, port)
|
6
|
+
{
|
7
|
+
ip: ip,
|
8
|
+
protocol: protocol,
|
9
|
+
port: port,
|
10
|
+
service_name: (
|
11
|
+
Socket.getservbyport(port.to_i, protocol) rescue nil
|
12
|
+
),
|
13
|
+
host: (
|
14
|
+
Resolv.getname(ip) rescue nil
|
15
|
+
)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
extend self
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
describe IdentifyUtil do
|
2
|
+
describe '::ip_protocol_port' do
|
3
|
+
before do
|
4
|
+
expect(Resolv).to receive(:getname).with('127.0.0.1').and_return('localhost')
|
5
|
+
expect(Socket).to receive(:getservbyport).with(53, 'tcp').and_return('domain')
|
6
|
+
end
|
7
|
+
subject { IdentifyUtil.ip_protocol_port('127.0.0.1', 'tcp', '53') }
|
8
|
+
it { expect(subject).to eq(ip: '127.0.0.1', protocol: 'tcp', port: '53', service_name: 'domain', host: 'localhost') }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'airbrake-ruby'
|
2
|
+
require 'yaml'
|
3
|
+
require 'time'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'fileutils'
|
6
|
+
require_relative './identify_util.rb'
|
7
|
+
|
8
|
+
class UnseenConnectionsMonitor
|
9
|
+
|
10
|
+
class FirewallWarning < StandardError
|
11
|
+
attr_reader :from, :to, :protocol, :port
|
12
|
+
|
13
|
+
def initialize(from:, to:, protocol:, port:)
|
14
|
+
super("unseen #{protocol} connection from #{from} to #{to}:#{port}")
|
15
|
+
@from, @to, @protocol, @port = from, to, protocol, port
|
16
|
+
end
|
17
|
+
|
18
|
+
def params
|
19
|
+
IdentifyUtil.ip_protocol_port(to, protocol, port).merge(from: from)
|
20
|
+
end
|
21
|
+
|
22
|
+
def ==(other)
|
23
|
+
message == other.message
|
24
|
+
end
|
25
|
+
alias_method :eq?, :==
|
26
|
+
end
|
27
|
+
|
28
|
+
DEFAULTS = {
|
29
|
+
remember_for: 604800,
|
30
|
+
save_interval: 3600,
|
31
|
+
state_file: "unseen_connections.yml",
|
32
|
+
networks: {
|
33
|
+
production: /.*/
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
attr_reader :config
|
38
|
+
|
39
|
+
def initialize(config:)
|
40
|
+
@config = config
|
41
|
+
@config.default_proc = proc { |h,k| h[k] = DEFAULTS[k] }
|
42
|
+
DEFAULTS.keys.each { |k| @config[k] }
|
43
|
+
configure_airbrake
|
44
|
+
end
|
45
|
+
|
46
|
+
def close
|
47
|
+
networks.each { |name| Airbrake.close(name) }
|
48
|
+
save
|
49
|
+
end
|
50
|
+
|
51
|
+
# taken from activesupport File::atomic_write
|
52
|
+
def save
|
53
|
+
state_file = config.fetch(:state_file)
|
54
|
+
tempfile = Tempfile.new(File.basename(state_file), Dir.tmpdir)
|
55
|
+
tempfile.write(state.to_yaml)
|
56
|
+
tempfile.close
|
57
|
+
begin
|
58
|
+
# Get original file permissions
|
59
|
+
old_stat = File.stat(state_file)
|
60
|
+
rescue Errno::ENOENT
|
61
|
+
# No old permissions, write a temp file to determine the defaults
|
62
|
+
check_name = File.join(File.dirname(file_name), ".permissions_check.#{Thread.current.object_id}.#{Process.pid}.#{rand(1000000)}")
|
63
|
+
File.open(check_name, "w") { }
|
64
|
+
old_stat = File.stat(check_name)
|
65
|
+
File.unlink(check_name)
|
66
|
+
end
|
67
|
+
FileUtils.mv(tempfile.path, state_file)
|
68
|
+
File.chown(old_stat.uid, old_stat.gid, state_file)
|
69
|
+
File.chmod(old_stat.mode, state_file)
|
70
|
+
@last_save = Time.now.to_i
|
71
|
+
end
|
72
|
+
|
73
|
+
def state
|
74
|
+
@state ||= begin
|
75
|
+
@last_save = Time.now.to_i
|
76
|
+
YAML.load_file(config.fetch(:state_file)) || {} rescue {}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def last_save
|
81
|
+
@last_save || 0
|
82
|
+
end
|
83
|
+
|
84
|
+
def feed(datetime:,from_ip:,protocol:,to_ip:,port:)
|
85
|
+
timestamp = Time.parse(datetime).to_i
|
86
|
+
protocol = protocol.downcase
|
87
|
+
key = "#{to_ip}:#{protocol}/#{port}"
|
88
|
+
|
89
|
+
warning = notify(from: from_ip, to: to_ip, protocol: protocol, port: port) unless state.has_key?(key)
|
90
|
+
state[key] = timestamp
|
91
|
+
warning
|
92
|
+
ensure
|
93
|
+
housekeeping
|
94
|
+
end
|
95
|
+
|
96
|
+
def networks
|
97
|
+
config.fetch(:networks).keys
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def notify(from:,to:,protocol:,port:)
|
103
|
+
warning = FirewallWarning.new(from: from, to: to, protocol: protocol, port: port)
|
104
|
+
Airbrake.notify(warning, warning.params, network_name(from) || :production)
|
105
|
+
warning
|
106
|
+
end
|
107
|
+
|
108
|
+
def network_name(from)
|
109
|
+
name, _ = config.fetch(:networks).find { |_, regexp| from =~ regexp }
|
110
|
+
name
|
111
|
+
end
|
112
|
+
|
113
|
+
def housekeeping
|
114
|
+
now = Time.now.to_i
|
115
|
+
state.reject! { |_, last_attempt| last_attempt < now - config.fetch(:remember_for) }
|
116
|
+
save if last_save < now - config.fetch(:save_interval)
|
117
|
+
end
|
118
|
+
|
119
|
+
def configure_airbrake
|
120
|
+
networks.each do |name, _|
|
121
|
+
Airbrake.configure(name) do |c|
|
122
|
+
c.project_id = config.fetch(:airbrake).fetch(:project_id)
|
123
|
+
c.project_key = config.fetch(:airbrake).fetch(:project_key)
|
124
|
+
c.environment = name
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: unseen-ip-to-airbrake
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lukas Rieder
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: airbrake-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.6.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.6'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.6.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rspec
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '3.5'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.5'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: timecop
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: pry-byebug
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
description: Capture unseen IP addresses connection attempts and report them to Airbrake
|
76
|
+
email: l.rieder@gmail.com
|
77
|
+
executables:
|
78
|
+
- identify-ip-protocol-port
|
79
|
+
- unseen-connections
|
80
|
+
extensions: []
|
81
|
+
extra_rdoc_files: []
|
82
|
+
files:
|
83
|
+
- bin/identify-ip-protocol-port
|
84
|
+
- bin/unseen-connections
|
85
|
+
- lib/identify_util.rb
|
86
|
+
- lib/identify_util_spec.rb
|
87
|
+
- lib/unseen_connections_monitor.rb
|
88
|
+
homepage: https://github.com/Overbryd/unseen-ip-to-airbrake
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
metadata: {}
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 2.6.8
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: Unseen IP to Airbrake
|
112
|
+
test_files: []
|