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