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,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