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 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: []