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,83 @@
1
+ require 'dm-core'
2
+
3
+ module DataMapper
4
+ class Property
5
+ class CryptFlags < DataMapper::Property::Integer
6
+ def self.flags
7
+ PatronusFati::SSID_CRYPT_MAP.values.map(&:to_sym)
8
+ end
9
+
10
+ attr_reader :flag_map
11
+
12
+ def custom?
13
+ true
14
+ end
15
+
16
+ #def dump(value)
17
+ # unless value.nil?
18
+ # flags = Array(value).map(&:to_s)
19
+ # flags.uniq!
20
+
21
+ # valid_values = flags & PatronusFati::SSID_CRYPT_MAP.values
22
+ # PatronusFati::SSID_CRYPT_MAP.map { |k, v| valid_values.include?(v) ? k : 0 }.inject(&:+)
23
+ # end
24
+ #end
25
+
26
+ def dump(value)
27
+ unless value.nil?
28
+ flags = Array(value).map { |flag| flag.to_sym }
29
+ flags.uniq!
30
+
31
+ flag = 0
32
+
33
+ flag_map.invert.values_at(*flags).each do |i|
34
+ next if i.nil?
35
+ flag += (1 << i)
36
+ end
37
+
38
+ flag
39
+ end
40
+ end
41
+
42
+ def initialize(model, name, options = {})
43
+ super
44
+
45
+ @flag_map = {}
46
+
47
+ flags = options.fetch(:flags, self.class.flags)
48
+ flags.each_with_index do |flag, i|
49
+ flag_map[i] = flag
50
+ end
51
+ end
52
+
53
+ #def load(value)
54
+ # return [] if value.nil?
55
+ # PatronusFati::SSID_CRYPT_MAP.select { |k, v| (k & value) == k }.map { |k, v| v }
56
+ #end
57
+
58
+ def load(value)
59
+ return [] if value.nil? || value <= 0
60
+
61
+ begin
62
+ matches = []
63
+
64
+ 0.upto(flag_map.size - 1) do |i|
65
+ matches << flag_map[i] if value[i] == 1
66
+ end
67
+
68
+ matches.compact
69
+ rescue TypeError, Errno::EDOM
70
+ []
71
+ end
72
+ end
73
+
74
+ def typecast(value)
75
+ case value
76
+ when nil then nil
77
+ when ::Array then value.map { |v| v.to_sym }
78
+ else [value.to_sym]
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,7 @@
1
+ module PatronusFati
2
+ module NullTablePrefix
3
+ def self.call(model_name)
4
+ DataMapper::NamingConventions::Resource::UnderscoredAndPluralized.call(model_name.to_s.split('::').last)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,74 @@
1
+ module PatronusFati::DataModels
2
+ class AccessPoint
3
+ include DataMapper::Resource
4
+
5
+ include PatronusFati::DataModels::ExpirationAttributes
6
+ include PatronusFati::DataModels::ReportedAttributes
7
+
8
+ property :id, Serial
9
+
10
+ property :bssid, String, :length => 17,
11
+ :required => true,
12
+ :unique => true
13
+
14
+ property :channel, Integer
15
+ property :max_seen_rate, Integer
16
+ property :type, String, :required => true
17
+
18
+ property :duplicate_iv_pkts, Integer, :default => 0
19
+ property :crypt_packets, Integer, :default => 0
20
+ property :data_packets, Integer, :default => 0
21
+ property :data_size, Integer, :default => 0
22
+
23
+ property :fragments, Integer, :default => 0
24
+ property :retries, Integer, :default => 0
25
+
26
+ property :range_ip, String
27
+ property :netmask, String
28
+ property :gateway_ip, String
29
+
30
+ has n, :clients, :through => :connections
31
+ has n, :connections, :constraint => :destroy,
32
+ :child_key => :access_point_id
33
+ has n, :ssids, :constraint => :destroy
34
+ has n, :ap_frequencies, :constraint => :destroy
35
+ has n, :ap_signals, :constraint => :destroy
36
+
37
+ belongs_to :mac, :required => false
38
+ before :save do
39
+ self.mac = Mac.first_or_create(mac: bssid)
40
+ end
41
+
42
+ def self.current_expiration_threshold
43
+ Time.now.to_i - PatronusFati::AP_EXPIRATION
44
+ end
45
+
46
+ def connected_clients
47
+ connections.active.clients
48
+ end
49
+
50
+ def current_ssids
51
+ ssids.active
52
+ end
53
+
54
+ def disconnect_clients!
55
+ connections.connected.map(&:disconnect!)
56
+ end
57
+
58
+ def full_state
59
+ blacklisted_keys = %w(id last_seen_at reported_online).map(&:to_sym)
60
+ attributes.reject { |k, v| blacklisted_keys.include?(k) || v.nil? }.merge(vendor: mac.vendor)
61
+ end
62
+
63
+ def record_signal(dbm)
64
+ PatronusFati::DataModels::ApSignal.create(access_point: self, dbm: dbm)
65
+ end
66
+
67
+ def update_frequencies(freq_hsh)
68
+ freq_hsh.each do |freq, packet_count|
69
+ f = ap_frequencies.first_or_create({mhz: freq}, {packet_count: packet_count})
70
+ f.update({packet_count: packet_count})
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,24 @@
1
+ module PatronusFati::DataModels
2
+ class Alert
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
6
+ property :created_at, Integer, :default => Proc.new { Time.now.to_i }
7
+ property :message, String, :length => 255
8
+
9
+ belongs_to :src_mac, :model => 'Mac', :required => false
10
+ belongs_to :dst_mac, :model => 'Mac', :required => false
11
+ belongs_to :other_mac, :model => 'Mac', :required => false
12
+
13
+ def full_state
14
+ {
15
+ created_at: created_at,
16
+ message: message,
17
+
18
+ source: src_mac.mac,
19
+ destination: dst_mac.mac,
20
+ other: other_mac.mac
21
+ }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module PatronusFati::DataModels
2
+ class ApFrequency
3
+ include DataMapper::Resource
4
+
5
+ property :mhz, Integer, :key => true,
6
+ :required => true
7
+ property :access_point_id, Integer, :key => true,
8
+ :required => true
9
+ property :packet_count, Integer, :default => 0,
10
+ :required => true
11
+
12
+ belongs_to :access_point
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module PatronusFati::DataModels
2
+ class ApSignal
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
6
+ property :timestamp, Integer, :default => Proc.new { Time.now.to_i },
7
+ :required => true
8
+ property :dbm, Integer, :required => true
9
+
10
+ belongs_to :access_point
11
+ end
12
+ end
@@ -0,0 +1,69 @@
1
+ module PatronusFati::DataModels
2
+ class Client
3
+ include DataMapper::Resource
4
+
5
+ include PatronusFati::DataModels::ExpirationAttributes
6
+ include PatronusFati::DataModels::ReportedAttributes
7
+
8
+ property :id, Serial
9
+ property :bssid, String, :length => 17, :unique => true
10
+ property :channel, Integer
11
+
12
+ property :crypt_packets, Integer, :default => 0
13
+ property :data_packets, Integer, :default => 0
14
+ property :data_size, Integer, :default => 0
15
+ property :fragments, Integer, :default => 0
16
+ property :retries, Integer, :default => 0
17
+
18
+ property :max_seen_rate, Integer
19
+
20
+ property :ip, String
21
+ property :gateway_ip, String
22
+ property :dhcp_host, String, :length => 64
23
+
24
+ has n, :connections, :constraint => :destroy
25
+ has n, :access_points, :through => :connections
26
+
27
+ has n, :client_frequencies, :constraint => :destroy
28
+ has n, :client_signals, :constraint => :destroy
29
+ has n, :probes, :constraint => :destroy
30
+
31
+ belongs_to :mac, :required => false
32
+ before :save do
33
+ self.mac = Mac.first_or_create(mac: bssid)
34
+ end
35
+
36
+ def self.current_expiration_threshold
37
+ Time.now.to_i - PatronusFati::CLIENT_EXPIRATION
38
+ end
39
+
40
+ def connected_access_points
41
+ connections.active.access_points
42
+ end
43
+
44
+ def disconnect!
45
+ connections.connected.map(&:disconnect!)
46
+ end
47
+
48
+ def full_state
49
+ blacklisted_keys = %w(id last_seen_at reported_online).map(&:to_sym)
50
+ base_attrs = attributes.reject { |k, v| blacklisted_keys.include?(k) || v.nil? }
51
+ base_attrs.merge(
52
+ connected_access_points: connected_access_points.map(&:bssid),
53
+ probes: probes.map(&:essid),
54
+ vendor: mac.vendor
55
+ )
56
+ end
57
+
58
+ def record_signal(dbm)
59
+ PatronusFati::DataModels::ClientSignal.create(client: self, dbm: dbm)
60
+ end
61
+
62
+ def update_frequencies(freq_hsh)
63
+ freq_hsh.each do |freq, packet_count|
64
+ f = client_frequencies.first_or_create({mhz: freq}, {packet_count: packet_count})
65
+ f.update({packet_count: packet_count})
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,14 @@
1
+ module PatronusFati::DataModels
2
+ class ClientFrequency
3
+ include DataMapper::Resource
4
+
5
+ property :mhz, Integer, :key => true,
6
+ :required => true
7
+ property :client_id, Integer, :key => true,
8
+ :required => true
9
+ property :packet_count, Integer, :default => 0,
10
+ :required => true
11
+
12
+ belongs_to :client
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module PatronusFati::DataModels
2
+ class ClientSignal
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
6
+ property :timestamp, Integer, :default => Proc.new { Time.now.to_i },
7
+ :required => true
8
+ property :dbm, Integer, :required => true
9
+
10
+ belongs_to :client
11
+ end
12
+ end
@@ -0,0 +1,49 @@
1
+ module PatronusFati
2
+ module DataModels
3
+ module ReportedAttributes
4
+ def self.included(klass)
5
+ klass.extend RAClassMethods
6
+ klass.property :reported_online, DataMapper::Property::Boolean, :default => false
7
+ end
8
+
9
+ module RAClassMethods
10
+ def reported_offline
11
+ all(:reported_online => false)
12
+ end
13
+
14
+ def reported_online
15
+ all(:reported_online => true)
16
+ end
17
+ end
18
+ end
19
+
20
+ module ExpirationAttributes
21
+ def self.included(klass)
22
+ klass.extend EAClassMethods
23
+ klass.property :last_seen_at, DataMapper::Property::Integer, :default => Proc.new { Time.now.to_i }
24
+ end
25
+
26
+ def active?
27
+ last_seen_at < self.class.current_expiration_threshold
28
+ end
29
+
30
+ def seen!
31
+ update(last_seen_at: Time.now.to_i)
32
+ end
33
+
34
+ def uptime
35
+ Time.now.to_i - last_seen_at
36
+ end
37
+
38
+ module EAClassMethods
39
+ def active
40
+ all(:last_seen_at.gte => current_expiration_threshold)
41
+ end
42
+
43
+ def inactive
44
+ all(:last_seen_at.lt => current_expiration_threshold)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ module PatronusFati::DataModels
2
+ class Connection
3
+ include DataMapper::Resource
4
+
5
+ include PatronusFati::DataModels::ExpirationAttributes
6
+
7
+ property :id, Serial
8
+
9
+ property :connected_at, Integer, :default => Proc.new { Time.now.to_i }
10
+ property :disconnected_at, Integer, :default => Proc.new { Time.now.to_i }
11
+ property :duration, Integer
12
+
13
+ belongs_to :access_point
14
+ belongs_to :client
15
+
16
+ def self.connected
17
+ all(:disconnected_at => nil)
18
+ end
19
+
20
+ def self.disconnected
21
+ all(:disconnected_at.not => nil)
22
+ end
23
+
24
+ def self.current_expiration_threshold
25
+ Time.now.to_i - PatronusFati::CONNECTION_EXPIRATION
26
+ end
27
+
28
+ def connected?
29
+ disconnected_at.nil?
30
+ end
31
+
32
+ def disconnect!
33
+ update(disconnected_at: Time.now.to_i, duration: duration) if connected?
34
+ end
35
+
36
+ def duration
37
+ self[:duration] || (Time.now.to_i - connected_at)
38
+ end
39
+
40
+ def full_state
41
+ {
42
+ access_point: access_point.bssid,
43
+ client: client.bssid,
44
+ connected: connected?
45
+ }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,48 @@
1
+ module PatronusFati::DataModels
2
+ class Mac
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
6
+
7
+ property :mac, String, :length => 17, :unique => true
8
+ property :vendor, String, :length => 255
9
+
10
+ property :alert_count, Integer, :default => 0
11
+ property :clients_connected, Integer, :default => 0
12
+ property :active_ssids, Integer, :default => 0
13
+ property :is_client, Boolean, :default => false
14
+ property :connections_to_ap, Integer, :default => 0
15
+
16
+ has n, :access_points
17
+ has n, :clients
18
+
19
+ has n, :dst_alerts, :model => 'Alert', :child_key => :dst_mac_id
20
+ has n, :other_alerts, :model => 'Alert', :child_key => :other_mac_id
21
+ has n, :src_alerts, :model => 'Alert', :child_key => :src_mac_id
22
+
23
+ before :save do
24
+ next if self.vendor
25
+
26
+ result = Louis.lookup(mac)
27
+ self.vendor = result['long_vendor'] || result['short_vendor']
28
+ end
29
+
30
+ def is_ap?
31
+ access_points.active.any?
32
+ end
33
+
34
+ def is_client?
35
+ clients.active.any?
36
+ end
37
+
38
+ def update_cached_counts!
39
+ update(
40
+ alert_count: (dst_alerts | other_alerts | src_alerts).count,
41
+ active_ssids: access_points.ssids.active.count,
42
+ clients_connected: access_points.connections.connected.count,
43
+ connections_to_ap: clients.connections.connected.count,
44
+ is_client: is_client?
45
+ )
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ module PatronusFati::DataModels
2
+ class Probe
3
+ include DataMapper::Resource
4
+
5
+ property :client_id, Integer, :key => true
6
+ property :essid, String, :key => true, :length => 64
7
+
8
+ property :first_seen_at, Integer, :default => Proc.new { Time.now.to_i }
9
+ property :last_seen_at, Integer, :default => Proc.new { Time.now.to_i }
10
+
11
+ belongs_to :client
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ module PatronusFati::DataModels
2
+ class Ssid
3
+ include DataMapper::Resource
4
+
5
+ include PatronusFati::DataModels::ExpirationAttributes
6
+ include PatronusFati::DataModels::ReportedAttributes
7
+
8
+ property :id, Serial
9
+
10
+ property :beacon_rate, Integer
11
+ property :beacon_info, String
12
+
13
+ property :cloaked, Boolean, :default => false
14
+ property :essid, String, :length => 64
15
+ property :crypt_set, CryptFlags
16
+ property :max_rate, Integer
17
+
18
+ belongs_to :access_point
19
+
20
+ def self.current_expiration_threshold
21
+ Time.now.to_i - PatronusFati::SSID_EXPIRATION
22
+ end
23
+
24
+ def full_state
25
+ {
26
+ beacon_info: beacon_info,
27
+ beacon_rate: beacon_rate,
28
+ cloaked: cloaked,
29
+ crypt_set: crypt_set,
30
+ essid: essid,
31
+ max_rate: max_rate
32
+ }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,53 @@
1
+ module PatronusFati::DataObservers
2
+ class AccessPointObserver
3
+ include DataMapper::Observer
4
+
5
+ observe PatronusFati::DataModels::AccessPoint
6
+
7
+ before :save do
8
+ next unless self.valid?
9
+
10
+ self.reported_online = active?
11
+
12
+ @change_type = self.new? ? :new : :changed
13
+
14
+ if @change_type == :changed
15
+ dirty = self.dirty_attributes.map { |a| a.first.name }.map(&:to_s)
16
+ dirty.select! { |k, _| full_state.keys.include?(k) || k == 'reported_online' }
17
+
18
+ # If there weren't any meaningful changes, don't print out anything
19
+ # after we save.
20
+ if dirty.empty?
21
+ @change_type = nil
22
+ next
23
+ end
24
+
25
+ changes = dirty.map do |attr|
26
+ clean = original_attributes[PatronusFati::DataModels::AccessPoint.properties[attr]]
27
+ dirty = dirty_attributes[PatronusFati::DataModels::AccessPoint.properties[attr]]
28
+
29
+ [attr, [clean, dirty]]
30
+ end
31
+
32
+ @change_list = Hash[changes]
33
+ @change_list.delete('reported_online')
34
+ end
35
+ end
36
+
37
+ after :save do
38
+ next unless @change_type
39
+
40
+ mac.update_cached_counts!
41
+
42
+ PatronusFati.event_handler.event(
43
+ :access_point,
44
+ @change_type,
45
+ self.full_state,
46
+ @change_list || {}
47
+ )
48
+
49
+ @change_type = nil
50
+ @change_list = nil
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,12 @@
1
+ module PatronusFati::DataObservers
2
+ class AlertObserver
3
+ include DataMapper::Observer
4
+
5
+ observe PatronusFati::DataModels::Alert
6
+
7
+ after :save do
8
+ [src_mac, dst_mac, other_mac].uniq.map(&:update_cached_counts!)
9
+ PatronusFati.event_handler.event(:alert, :new, self.full_state)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,52 @@
1
+ module PatronusFati::DataObservers
2
+ class ClientObserver
3
+ include DataMapper::Observer
4
+
5
+ observe PatronusFati::DataModels::Client
6
+
7
+ before :save do
8
+ break unless self.valid?
9
+
10
+ self.reported_online = active?
11
+
12
+ @change_type = self.new? ? :new : :changed
13
+ if @change_type == :changed
14
+ dirty = self.dirty_attributes.map { |a| a.first.name }.map(&:to_s)
15
+ dirty.select! { |k, _| full_state.keys.include?(k) || k == 'reported_online' }
16
+
17
+ # If there weren't any meaningful changes, don't print out anything
18
+ # after we save.
19
+ if dirty.empty?
20
+ @change_type = nil
21
+ next
22
+ end
23
+
24
+ changes = dirty.map do |attr|
25
+ clean = original_attributes[PatronusFati::DataModels::Client.properties[attr]]
26
+ dirty = dirty_attributes[PatronusFati::DataModels::Client.properties[attr]]
27
+
28
+ [attr, [clean, dirty]]
29
+ end
30
+
31
+ @change_list = Hash[changes]
32
+ @change_list.delete('reported_online')
33
+ end
34
+ end
35
+
36
+ after :save do
37
+ next unless @change_type
38
+
39
+ mac.update_cached_counts!
40
+
41
+ PatronusFati.event_handler.event(
42
+ :client,
43
+ @change_type,
44
+ self.full_state,
45
+ @change_list || {}
46
+ )
47
+
48
+ @change_type = nil
49
+ @change_list = nil
50
+ end
51
+ end
52
+ end