patronus_fati 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +165 -0
  7. data/README.md +29 -0
  8. data/Rakefile +21 -0
  9. data/bin/patronus_fati +54 -0
  10. data/kismet_configs/pat_fat_startup.sh +3 -0
  11. data/kismet_configs/patronus_fati_kismet.conf +88 -0
  12. data/lib/patronus_fati/cap_struct.rb +97 -0
  13. data/lib/patronus_fati/connection.rb +85 -0
  14. data/lib/patronus_fati/consts.rb +85 -0
  15. data/lib/patronus_fati/data_mapper/crypt_flags.rb +83 -0
  16. data/lib/patronus_fati/data_mapper/null_table_prefix.rb +7 -0
  17. data/lib/patronus_fati/data_models/access_point.rb +74 -0
  18. data/lib/patronus_fati/data_models/alert.rb +24 -0
  19. data/lib/patronus_fati/data_models/ap_frequency.rb +14 -0
  20. data/lib/patronus_fati/data_models/ap_signal.rb +12 -0
  21. data/lib/patronus_fati/data_models/client.rb +69 -0
  22. data/lib/patronus_fati/data_models/client_frequency.rb +14 -0
  23. data/lib/patronus_fati/data_models/client_signal.rb +12 -0
  24. data/lib/patronus_fati/data_models/common.rb +49 -0
  25. data/lib/patronus_fati/data_models/connection.rb +48 -0
  26. data/lib/patronus_fati/data_models/mac.rb +48 -0
  27. data/lib/patronus_fati/data_models/probe.rb +13 -0
  28. data/lib/patronus_fati/data_models/ssid.rb +35 -0
  29. data/lib/patronus_fati/data_observers/access_point_observer.rb +53 -0
  30. data/lib/patronus_fati/data_observers/alert_observer.rb +12 -0
  31. data/lib/patronus_fati/data_observers/client_observer.rb +52 -0
  32. data/lib/patronus_fati/data_observers/connection_observer.rb +66 -0
  33. data/lib/patronus_fati/data_observers/probe_observer.rb +11 -0
  34. data/lib/patronus_fati/data_observers/ssid_observer.rb +53 -0
  35. data/lib/patronus_fati/event_handler.rb +27 -0
  36. data/lib/patronus_fati/factory_base.rb +56 -0
  37. data/lib/patronus_fati/message_models/ack.rb +5 -0
  38. data/lib/patronus_fati/message_models/alert.rb +10 -0
  39. data/lib/patronus_fati/message_models/battery.rb +6 -0
  40. data/lib/patronus_fati/message_models/bssid.rb +43 -0
  41. data/lib/patronus_fati/message_models/bssidsrc.rb +15 -0
  42. data/lib/patronus_fati/message_models/btscandev.rb +11 -0
  43. data/lib/patronus_fati/message_models/capability.rb +5 -0
  44. data/lib/patronus_fati/message_models/channel.rb +13 -0
  45. data/lib/patronus_fati/message_models/client.rb +45 -0
  46. data/lib/patronus_fati/message_models/clisrc.rb +17 -0
  47. data/lib/patronus_fati/message_models/clitag.rb +6 -0
  48. data/lib/patronus_fati/message_models/common.rb +15 -0
  49. data/lib/patronus_fati/message_models/critfail.rb +5 -0
  50. data/lib/patronus_fati/message_models/error.rb +6 -0
  51. data/lib/patronus_fati/message_models/gps.rb +6 -0
  52. data/lib/patronus_fati/message_models/info.rb +11 -0
  53. data/lib/patronus_fati/message_models/kismet.rb +8 -0
  54. data/lib/patronus_fati/message_models/nettag.rb +6 -0
  55. data/lib/patronus_fati/message_models/packet.rb +10 -0
  56. data/lib/patronus_fati/message_models/plugin.rb +8 -0
  57. data/lib/patronus_fati/message_models/protocols.rb +5 -0
  58. data/lib/patronus_fati/message_models/remove.rb +6 -0
  59. data/lib/patronus_fati/message_models/source.rb +10 -0
  60. data/lib/patronus_fati/message_models/spectrum.rb +8 -0
  61. data/lib/patronus_fati/message_models/ssid.rb +25 -0
  62. data/lib/patronus_fati/message_models/status.rb +5 -0
  63. data/lib/patronus_fati/message_models/string.rb +6 -0
  64. data/lib/patronus_fati/message_models/terminate.rb +5 -0
  65. data/lib/patronus_fati/message_models/time.rb +6 -0
  66. data/lib/patronus_fati/message_models/trackinfo.rb +8 -0
  67. data/lib/patronus_fati/message_models/wepkey.rb +6 -0
  68. data/lib/patronus_fati/message_models.rb +39 -0
  69. data/lib/patronus_fati/message_parser.rb +44 -0
  70. data/lib/patronus_fati/message_processor/alert.rb +15 -0
  71. data/lib/patronus_fati/message_processor/bssid.rb +47 -0
  72. data/lib/patronus_fati/message_processor/capability.rb +24 -0
  73. data/lib/patronus_fati/message_processor/client.rb +55 -0
  74. data/lib/patronus_fati/message_processor/critfail.rb +8 -0
  75. data/lib/patronus_fati/message_processor/error.rb +7 -0
  76. data/lib/patronus_fati/message_processor/protocols.rb +7 -0
  77. data/lib/patronus_fati/message_processor/ssid.rb +48 -0
  78. data/lib/patronus_fati/message_processor.rb +52 -0
  79. data/lib/patronus_fati/version.rb +3 -0
  80. data/lib/patronus_fati.rb +68 -0
  81. data/patronus_fati.gemspec +41 -0
  82. data/spec/data_models/access_point_spec.rb +26 -0
  83. data/spec/data_models/alert_spec.rb +12 -0
  84. data/spec/data_models/client_spec.rb +25 -0
  85. data/spec/data_models/connection_spec.rb +86 -0
  86. data/spec/data_models/mac_spec.rb +26 -0
  87. data/spec/patronus_fati_spec.rb +13 -0
  88. data/spec/spec_helper.rb +71 -0
  89. data/wrapper.rb +19 -0
  90. metadata +393 -0
@@ -0,0 +1,44 @@
1
+ module PatronusFati
2
+ module MessageParser
3
+ # We receive some messages before we specifically request the abilities of
4
+ # the server, when this happens we'll attempt to map the data using the
5
+ # default attribute ordering that was provided by the Kismet server this
6
+ # client was coded against, this may not be entirely accurate, but will
7
+ # become accurate before we receive any meaningful data.
8
+ def self.parse(msg)
9
+ return unless (raw_data = handle_msg(msg))
10
+
11
+ unless (cap = get_model(raw_data[0]))
12
+ warn('Message received had unknown message type: ' + raw_data[0])
13
+ return
14
+ end
15
+
16
+ src_keys = cap.enabled_keys.empty? ? cap.attribute_keys : cap.enabled_keys
17
+ cap.new(Hash[src_keys.zip(raw_data[1])])
18
+ end
19
+
20
+ protected
21
+
22
+ def self.extract_data(data_line)
23
+ data_line.scan(PatronusFati::DATA_DELIMITER).map { |a, b| (a || b).tr("\x01", '') }
24
+ end
25
+
26
+ def self.get_model(mdl)
27
+ return unless PatronusFati::MessageModels.const_defined?(model_name(mdl))
28
+ PatronusFati::MessageModels.const_get(model_name(mdl))
29
+ end
30
+
31
+ def self.handle_msg(line)
32
+ resp = PatronusFati::SERVER_MESSAGE.match(line)
33
+ return unless resp
34
+
35
+ h = Hash[resp.names.zip(resp.captures)]
36
+
37
+ [h['header'], extract_data(h['data'])]
38
+ end
39
+
40
+ def self.model_name(hdr)
41
+ hdr.downcase.capitalize.to_sym
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ module PatronusFati::MessageProcessor::Alert
2
+ include PatronusFati::MessageProcessor
3
+
4
+ def self.process(obj)
5
+ src_mac = PatronusFati::DataModels::Mac.first_or_create(mac: obj[:source])
6
+ dst_mac = PatronusFati::DataModels::Mac.first_or_create(mac: obj[:dest])
7
+ other_mac = PatronusFati::DataModels::Mac.first_or_create(mac: obj[:other])
8
+
9
+ PatronusFati::DataModels::Alert.first_or_create({created_at: obj.sec, \
10
+ message: obj[:text]}, {created_at: obj.sec, message: obj[:text], \
11
+ src_mac: src_mac, dst_mac: dst_mac, other_mac: other_mac})
12
+
13
+ nil
14
+ end
15
+ end
@@ -0,0 +1,47 @@
1
+ module PatronusFati::MessageProcessor::Bssid
2
+ include PatronusFati::MessageProcessor
3
+
4
+ def self.process(obj)
5
+ # We don't care about objects that would have expired already...
6
+ return if obj[:lasttime] < (Time.now.to_i - PatronusFati::AP_EXPIRATION)
7
+
8
+ # Ignore probe requests as their BSSID information is useless (the ESSID
9
+ # isn't present and it's coming from a client).
10
+ return unless %w(infrastructure adhoc).include?(obj.type.to_s)
11
+
12
+ ap_info = ap_data(obj.attributes)
13
+ access_point = PatronusFati::DataModels::AccessPoint.first_or_create({bssid: obj.bssid}, ap_info)
14
+
15
+ access_point.update(ap_info)
16
+ access_point.record_signal(obj.signal_dbm)
17
+ access_point.update_frequencies(obj.freqmhz)
18
+
19
+ nil
20
+ end
21
+
22
+ protected
23
+
24
+ def self.ap_data(attrs)
25
+ {
26
+ bssid: attrs[:bssid],
27
+ type: attrs[:type],
28
+ channel: attrs[:channel],
29
+
30
+ crypt_packets: attrs[:cryptpackets],
31
+ data_packets: attrs[:datapackets],
32
+ data_size: attrs[:datasize],
33
+
34
+ fragments: attrs[:fragments],
35
+ retries: attrs[:retries],
36
+
37
+ max_seen_rate: attrs[:maxseenrate],
38
+ duplicate_iv_pkts: attrs[:dupeivpackets],
39
+
40
+ range_ip: attrs[:rangeip],
41
+ netmask: attrs[:netmaskip],
42
+ gateway_ip: attrs[:gatewayip],
43
+
44
+ last_seen_at: Time.now.to_i
45
+ }.reject { |_, v| v.nil? }
46
+ end
47
+ end
@@ -0,0 +1,24 @@
1
+ module PatronusFati::MessageProcessor::Capability
2
+ include PatronusFati::MessageProcessor
3
+
4
+ def self.process(obj)
5
+ # The capability detection for the capability command is broken. It
6
+ # returns the name of the command followed by the capabilities but the
7
+ # result of a request ignores that it also sends back the name of the
8
+ # command. We don't want to mess up our parsing so we work around it by
9
+ # ignoring these messages.
10
+ return if obj.name == 'CAPABILITY'
11
+ return unless PatronusFati::MessageModels.const_defined?(obj.name.downcase.capitalize)
12
+
13
+ target_cap = PatronusFati::MessageModels.const_get(obj.name.downcase.capitalize)
14
+ target_cap.supported_keys = obj.capabilities.split(',').map(&:to_sym)
15
+
16
+ keys_to_enable = target_cap.enabled_keys.map(&:to_s).join(',')
17
+
18
+ # Limit the amount of data kismet gives us to only the interesting stuff
19
+ return unless %w(ERROR PROTOCOLS ALERT BSSID SSID CLIENT CRITFAIL).include?(obj.name)
20
+
21
+ # Return the response to the server
22
+ "ENABLE #{obj.name} #{keys_to_enable}"
23
+ end
24
+ end
@@ -0,0 +1,55 @@
1
+ module PatronusFati::MessageProcessor::Client
2
+ include PatronusFati::MessageProcessor
3
+
4
+ def self.process(obj)
5
+ # We don't care about objects that would have expired already...
6
+ return if obj[:lasttime] < PatronusFati::DataModels::Client.current_expiration_threshold
7
+
8
+ client_info = client_data(obj.attributes)
9
+
10
+ client = PatronusFati::DataModels::Client.first_or_create({bssid: obj[:mac]}, client_info)
11
+ client.update(client_info)
12
+
13
+ client.record_signal(obj.signal_dbm)
14
+ client.update_frequencies(obj.freqmhz)
15
+
16
+ # Don't deal in associations that are outside of our connection expiration
17
+ # time...
18
+ return if obj[:lasttime] <= (Time.now.to_i - PatronusFati::CONNECTION_EXPIRATION)
19
+
20
+ # Handle the associations
21
+ unless obj[:bssid].nil? || obj[:bssid].empty? || obj[:bssid] == obj[:mac]
22
+ return unless (ap = PatronusFati::DataModels::AccessPoint.first(bssid: obj[:bssid]))
23
+ ap.seen!
24
+
25
+ conn = PatronusFati::DataModels::Connection.connected.first_or_create({client: client, access_point: ap})
26
+ conn.seen!
27
+ end
28
+
29
+ nil
30
+ end
31
+
32
+ protected
33
+
34
+ def self.client_data(attrs)
35
+ {
36
+ bssid: attrs[:mac],
37
+ channel: attrs[:channel],
38
+
39
+ crypt_packets: attrs[:cryptpackets],
40
+ data_packets: attrs[:datapackets],
41
+ data_size: attrs[:datasize],
42
+
43
+ fragments: attrs[:fragments],
44
+ retries: attrs[:retries],
45
+
46
+ max_seen_rate: attrs[:maxseenrate],
47
+
48
+ ip: attrs[:ip],
49
+ gateway_ip: attrs[:gatewayip],
50
+ dhcp_host: attrs[:dhcphost],
51
+
52
+ last_seen_at: Time.now.to_i
53
+ }.reject { |_, v| v.nil? }
54
+ end
55
+ end
@@ -0,0 +1,8 @@
1
+ module PatronusFati::MessageProcessor::Critfail
2
+ include PatronusFati::MessageProcessor
3
+
4
+ def self.process(opts)
5
+ puts ('Critical fail message: %s' % opts.message)
6
+ nil
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module PatronusFati::MessageProcessor::Error
2
+ include PatronusFati::MessageProcessor
3
+
4
+ def self.process(opts)
5
+ warn('Failed command ID %i with error: %s' % [opts.cmdid, opts.text])
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module PatronusFati::MessageProcessor::Protocols
2
+ include PatronusFati::MessageProcessor
3
+
4
+ def self.process(obj)
5
+ obj.protocols.split(',').map { |p| "CAPABILITY #{p}" }
6
+ end
7
+ end
@@ -0,0 +1,48 @@
1
+ module PatronusFati::MessageProcessor::Ssid
2
+ include PatronusFati::MessageProcessor
3
+
4
+ def self.process(obj)
5
+ # We don't care about objects that would have expired already...
6
+ return if obj[:lasttime] < (Time.now.to_i - PatronusFati::SSID_EXPIRATION)
7
+
8
+ ssid_info = ssid_data(obj.attributes)
9
+
10
+ if %w(beacon probe_response).include?(obj[:type])
11
+ access_point = PatronusFati::DataModels::AccessPoint.first(bssid: obj[:mac])
12
+ return unless access_point # Only happens with a corrupt message
13
+
14
+ ssid = PatronusFati::DataModels::Ssid.first_or_create({access_point: access_point, essid: ssid_info[:essid]}, ssid_info)
15
+ ssid.update(ssid_info)
16
+ access_point.seen!
17
+ elsif obj[:type] == 'probe_request'
18
+ client = PatronusFati::DataModels::Client.first(bssid: obj[:mac])
19
+
20
+ return if client.nil?
21
+ client.seen!
22
+
23
+ return if obj[:ssid].nil? || obj[:ssid].empty?
24
+ client.probes.first_or_create(essid: obj[:ssid])
25
+ else
26
+ # The only thing left is the 'file' type which no one seems to understand
27
+ #puts ('Unknown SSID type (%s): %s' % [obj[:type], obj.inspect])
28
+ end
29
+
30
+ nil
31
+ end
32
+
33
+ protected
34
+
35
+ def self.ssid_data(attrs)
36
+ {
37
+ beacon_info: attrs[:beaconinfo],
38
+ beacon_rate: attrs[:beaconrate],
39
+
40
+ cloaked: attrs[:cloaked],
41
+ crypt_set: attrs[:cryptset].map(&:to_s),
42
+ essid: attrs[:ssid],
43
+ max_rate: attrs[:maxrate],
44
+
45
+ last_seen_at: Time.now.to_i
46
+ }.reject { |_, v| v.nil? }
47
+ end
48
+ end
@@ -0,0 +1,52 @@
1
+ module PatronusFati
2
+ module MessageProcessor
3
+ extend FactoryBase
4
+
5
+ def self.cleanup_models
6
+ @last_cleanup ||= Time.now.to_i
7
+
8
+ if @last_cleanup < (Time.now.to_i - 10)
9
+ @last_cleanup = Time.now.to_i
10
+
11
+ PatronusFati::DataModels::AccessPoint.inactive.reported_online.each do |ap|
12
+ ap.update(:reported_online => false)
13
+ PatronusFati.event_handler.event(:access_point, :offline, {'bssid' => ap.bssid, 'uptime' => ap.uptime})
14
+ ap.disconnect_clients!
15
+ end
16
+
17
+ PatronusFati::DataModels::Client.inactive.reported_online.each do |cli|
18
+ cli.update(:reported_online => false)
19
+ PatronusFati.event_handler.event(:client, :offline, {'bssid' => cli.bssid, 'uptime' => cli.uptime})
20
+ cli.disconnect!
21
+ end
22
+
23
+ PatronusFati::DataModels::Connection.inactive.connected.map(&:disconnect!)
24
+ end
25
+ end
26
+
27
+ def self.handle(message_obj)
28
+ result = factory(class_to_name(message_obj), message_obj)
29
+ cleanup_models
30
+ result
31
+ rescue => e
32
+ puts 'Error processing the following message object:'
33
+ puts message_obj.inspect
34
+ puts '%s: %s' % [e.class, e.message]
35
+ puts e.backtrace.join("\n")
36
+ end
37
+
38
+ def self.ignored_types
39
+ [:ack, :battery, :bssidsrc, :channel, :clisrc, :gps, :info, :kismet,
40
+ :plugin, :source, :status, :time]
41
+ end
42
+ end
43
+ end
44
+
45
+ require 'patronus_fati/message_processor/alert'
46
+ require 'patronus_fati/message_processor/bssid'
47
+ require 'patronus_fati/message_processor/capability'
48
+ require 'patronus_fati/message_processor/client'
49
+ require 'patronus_fati/message_processor/critfail'
50
+ require 'patronus_fati/message_processor/error'
51
+ require 'patronus_fati/message_processor/protocols'
52
+ require 'patronus_fati/message_processor/ssid'
@@ -0,0 +1,3 @@
1
+ module PatronusFati
2
+ VERSION = '0.8.0'
3
+ end
@@ -0,0 +1,68 @@
1
+ STDOUT.sync = true
2
+
3
+ require 'date'
4
+ require 'digest'
5
+ require 'json'
6
+ require 'openssl'
7
+ require 'socket'
8
+ require 'timeout'
9
+ require 'thread'
10
+
11
+ require 'dm-constraints'
12
+ require 'dm-core'
13
+ require 'dm-migrations'
14
+ require 'dm-observer'
15
+ require 'dm-timestamps'
16
+ require 'dm-validations'
17
+
18
+ require 'louis'
19
+
20
+ require 'patronus_fati/consts'
21
+ require 'patronus_fati/version'
22
+
23
+ require 'patronus_fati/data_mapper/crypt_flags'
24
+ require 'patronus_fati/data_mapper/null_table_prefix'
25
+
26
+ require 'patronus_fati/cap_struct'
27
+ require 'patronus_fati/connection'
28
+ require 'patronus_fati/event_handler'
29
+ require 'patronus_fati/factory_base'
30
+ require 'patronus_fati/message_models'
31
+ require 'patronus_fati/message_parser'
32
+ require 'patronus_fati/message_processor'
33
+
34
+ require 'patronus_fati/data_models/common'
35
+
36
+ require 'patronus_fati/data_models/access_point'
37
+ require 'patronus_fati/data_models/ap_frequency'
38
+ require 'patronus_fati/data_models/ap_signal'
39
+ require 'patronus_fati/data_models/alert'
40
+ require 'patronus_fati/data_models/client'
41
+ require 'patronus_fati/data_models/client_frequency'
42
+ require 'patronus_fati/data_models/client_signal'
43
+ require 'patronus_fati/data_models/connection'
44
+ require 'patronus_fati/data_models/mac'
45
+ require 'patronus_fati/data_models/probe'
46
+ require 'patronus_fati/data_models/ssid'
47
+
48
+ require 'patronus_fati/data_observers/access_point_observer'
49
+ require 'patronus_fati/data_observers/alert_observer'
50
+ require 'patronus_fati/data_observers/client_observer'
51
+ require 'patronus_fati/data_observers/connection_observer'
52
+ require 'patronus_fati/data_observers/ssid_observer'
53
+
54
+ module PatronusFati
55
+ def self.event_handler
56
+ @event_handler ||= PatronusFati::EventHandler.new
57
+ end
58
+
59
+ def self.setup(kismet_server, kismet_port, database_uri)
60
+ #DataMapper::Logger.new('pf-db.log', :debug)
61
+ DataMapper.setup(:default, database_uri)
62
+ DataMapper.repository(:default).adapter.resource_naming_convention = PatronusFati::NullTablePrefix
63
+ DataMapper.finalize
64
+ DataMapper.auto_upgrade!
65
+
66
+ PatronusFati::Connection.new(kismet_server, kismet_port)
67
+ end
68
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'patronus_fati/version'
6
+
7
+ Gem::Specification.new do |gem|
8
+ gem.name = "patronus_fati"
9
+ gem.version = PatronusFati::VERSION
10
+ gem.authors = [ "Sam Stelfox" ]
11
+ gem.license = "LGPL"
12
+ gem.email = [ "sstelfox@bedroomprogrammers.net" ]
13
+ gem.description = %q{ A ruby implementation of the Kismet client protocol. }
14
+ gem.summary = %q{ A ruby implementation of the Kismet client protocol. }
15
+ gem.homepage = ""
16
+
17
+ gem.files = `git ls-files`.split($/)
18
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
19
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
+ gem.require_paths = ["lib"]
21
+
22
+ gem.add_dependency 'dm-constraints'
23
+ gem.add_dependency 'dm-core'
24
+ gem.add_dependency 'dm-migrations'
25
+ gem.add_dependency 'dm-observer'
26
+ gem.add_dependency 'dm-sqlite-adapter'
27
+ gem.add_dependency 'dm-timestamps'
28
+ gem.add_dependency 'dm-validations'
29
+ gem.add_dependency 'louis', '~> 2.0'
30
+
31
+ gem.add_development_dependency 'database_cleaner'
32
+ gem.add_development_dependency 'dm-rspec'
33
+ gem.add_development_dependency 'dm-transactions'
34
+ gem.add_development_dependency 'rake'
35
+ gem.add_development_dependency 'rdoc'
36
+ gem.add_development_dependency 'pry'
37
+ gem.add_development_dependency 'redcarpet'
38
+ gem.add_development_dependency 'rspec'
39
+ gem.add_development_dependency 'simplecov'
40
+ gem.add_development_dependency 'yard'
41
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'DataModels::AccessPoint' do
4
+ subject { PatronusFati::DataModels::AccessPoint }
5
+
6
+ let(:unsaved_instance) { subject.new(bssid: '12:34:56:00:00:01', type: 'infrastructure', channel: 1) }
7
+ let(:saved_instance) { unsaved_instance.save }
8
+
9
+ it { expect(subject).to have_property(:bssid) }
10
+ it { expect(subject).to have_property(:type) }
11
+ it { expect(subject).to have_property(:channel) }
12
+ it { expect(subject).to have_property(:last_seen_at) }
13
+
14
+ it { expect(subject).to have_many(:connections) }
15
+ it { expect(subject).to have_many(:ssids) }
16
+
17
+ it { expect(subject).to have_many(:clients).through(:connections) }
18
+
19
+ it { expect(subject).to belong_to(:mac) }
20
+
21
+ it 'should associate to a MAC object before saving' do
22
+ expect(unsaved_instance.mac).to be_nil
23
+ unsaved_instance.save
24
+ expect(unsaved_instance.mac).to be_instance_of(PatronusFati::DataModels::Mac)
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'DataModels::Alert' do
4
+ subject { PatronusFati::DataModels::Alert }
5
+
6
+ it { expect(subject).to have_property(:created_at) }
7
+ it { expect(subject).to have_property(:message) }
8
+
9
+ it { expect(subject).to belong_to(:src_mac) }
10
+ it { expect(subject).to belong_to(:dst_mac) }
11
+ it { expect(subject).to belong_to(:other_mac) }
12
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'DataModels::Client' do
4
+ subject { PatronusFati::DataModels::Client }
5
+
6
+ let(:unsaved_instance) { subject.new(bssid: '12:34:56:00:00:02') }
7
+ let(:saved_instance) { unsaved_instance.save }
8
+
9
+ it { expect(subject).to have_property(:id) }
10
+ it { expect(subject).to have_property(:bssid) }
11
+ it { expect(subject).to have_property(:last_seen_at) }
12
+
13
+ it { expect(subject).to have_many(:probes) }
14
+
15
+ it { expect(subject).to have_many(:connections) }
16
+ it { expect(subject).to have_many(:access_points).through(:connections) }
17
+
18
+ it { expect(subject).to belong_to(:mac) }
19
+
20
+ it 'should associate to a MAC object before saving' do
21
+ expect(unsaved_instance.mac).to be_nil
22
+ unsaved_instance.save
23
+ expect(unsaved_instance.mac).to be_instance_of(PatronusFati::DataModels::Mac)
24
+ end
25
+ end
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'DataModels::Connection' do
4
+ subject { PatronusFati::DataModels::Connection }
5
+
6
+ let(:client_model) { PatronusFati::DataModels::Client }
7
+ let(:unsaved_client) { client_model.new(bssid: '11:22:33:00:00:00') }
8
+
9
+ let(:ap_model) { PatronusFati::DataModels::AccessPoint }
10
+ let(:unsaved_ap) { ap_model.new(bssid: '22:33:44:00:00:01', type: 'adhoc', channel: 6) }
11
+
12
+ let(:instance) { subject.new(client: unsaved_client, access_point: unsaved_ap) }
13
+
14
+ it { expect(subject).to have_property(:id) }
15
+
16
+ it { expect(subject).to have_property(:connected_at) }
17
+ it { expect(subject).to have_property(:disconnected_at) }
18
+
19
+ it { expect(subject).to belong_to(:access_point) }
20
+ it { expect(subject).to belong_to(:client) }
21
+
22
+ context '#active?' do
23
+ it 'should be true when a disconnection hasn\'t been registered' do
24
+ inst = instance
25
+ inst.disconnected_at = nil
26
+
27
+ expect(inst).to be_active
28
+ end
29
+
30
+ it 'should be false when a disconnection has been registered' do
31
+ inst = instance
32
+ inst.disconnected_at = Time.now
33
+
34
+ expect(inst).to_not be_active
35
+ end
36
+ end
37
+
38
+ context '#disconnect!' do
39
+ it 'should change an active instance to be inactive' do
40
+ inst = instance
41
+ inst.save
42
+
43
+ expect(inst).to be_active
44
+ inst.disconnect!
45
+ expect(inst).to_not be_active
46
+ end
47
+ end
48
+
49
+ context '#active scope' do
50
+ it 'should include active connections' do
51
+ inst = instance
52
+ inst.save
53
+
54
+ expect(inst).to be_active
55
+ expect(subject.active).to include(inst)
56
+ end
57
+
58
+ it 'should not include inactive connections' do
59
+ inst = instance
60
+ inst.save
61
+ inst.disconnect!
62
+
63
+ expect(inst).to_not be_active
64
+ expect(subject.active).to_not include(inst)
65
+ end
66
+ end
67
+
68
+ context '#inactive scope' do
69
+ it 'should not include active connections' do
70
+ inst = instance
71
+ inst.save
72
+
73
+ expect(inst).to be_active
74
+ expect(subject.inactive).to_not include(inst)
75
+ end
76
+
77
+ it 'should include inactive connections' do
78
+ inst = instance
79
+ inst.save
80
+ inst.disconnect!
81
+
82
+ expect(inst).to_not be_active
83
+ expect(subject.inactive).to include(inst)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'DataModels::Mac' do
4
+ subject { PatronusFati::DataModels::Mac }
5
+
6
+ let(:unsaved_instance) { subject.new(mac: '00:12:34:00:00:00') }
7
+ let(:saved_instance) { unsaved_instance.save }
8
+
9
+ it { expect(subject).to have_property(:mac) }
10
+ it { expect(subject).to have_property(:vendor) }
11
+
12
+ it { expect(subject).to have_many(:access_points) }
13
+ it { expect(subject).to have_many(:clients) }
14
+
15
+ it { expect(subject).to have_many(:dst_alerts) }
16
+ it { expect(subject).to have_many(:other_alerts) }
17
+ it { expect(subject).to have_many(:src_alerts) }
18
+
19
+ # This is a tad bit annoying, data mapper generates Ruby warnings about
20
+ # uninitialized instance variables.
21
+ it 'should set the vendor on the MAC object before saving' do
22
+ expect(unsaved_instance.vendor).to be_nil
23
+ unsaved_instance.save
24
+ expect(unsaved_instance.vendor).to_not be_nil
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe PatronusFati do
4
+ context 'VERSION' do
5
+ it 'should have a version' do
6
+ expect { PatronusFati::VERSION }.to_not raise_error
7
+ end
8
+
9
+ it 'should be properly formatted' do
10
+ expect(PatronusFati::VERSION).to match(/\d+\.\d+\.\d+([a-z])?/)
11
+ end
12
+ end
13
+ end