patronus_fati 0.8.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.
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