unseen-ip-to-airbrake 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|