Class: PatronusFati::DataModels::AccessPoint

Inherits:
Object
  • Object
show all
Includes:
CommonState
Defined in:
lib/patronus_fati/data_models/access_point.rb

Constant Summary collapse

LOCAL_ATTRIBUTE_KEYS =
[ :bssid, :channel, :type ].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from CommonState

#active?, #data_dirty?, #dirty?, included, #new?, #set_sync_flag, #status_dirty?, #sync_flag?

Constructor Details

#initialize(bssid) ⇒ AccessPoint

Returns a new instance of AccessPoint.



112
113
114
115
116
# File 'lib/patronus_fati/data_models/access_point.rb', line 112

def initialize(bssid)
  super
  self.local_attributes = { bssid: bssid }
  self.client_macs = []
end

Instance Attribute Details

#client_macsvoid

Returns the value of attribute client_macs.



6
7
8
# File 'lib/patronus_fati/data_models/access_point.rb', line 6

def client_macs
  @client_macs
end

#last_dbmvoid

Returns the value of attribute last_dbm.



6
7
8
# File 'lib/patronus_fati/data_models/access_point.rb', line 6

def last_dbm
  @last_dbm
end

#local_attributesvoid

Returns the value of attribute local_attributes.



6
7
8
# File 'lib/patronus_fati/data_models/access_point.rb', line 6

def local_attributes
  @local_attributes
end

#ssidsvoid

Returns the value of attribute ssids.



6
7
8
# File 'lib/patronus_fati/data_models/access_point.rb', line 6

def ssids
  @ssids
end

Class Method Details

.current_expiration_thresholdvoid



10
11
12
# File 'lib/patronus_fati/data_models/access_point.rb', line 10

def self.current_expiration_threshold
  Time.now.to_i - AP_EXPIRATION
end

Instance Method Details

#active_ssidsvoid



14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/patronus_fati/data_models/access_point.rb', line 14

def active_ssids
  return unless ssids
  # If there is any active SSIDs return them
  active = ssids.select { |_, v| v.active? }
  return active unless active.empty?

  # If there are no active SSIDs try and find the most recently seen SSID
  # and report that as still active. Still return an empty set if there
  # are no SSIDs.
  most_recent = ssids.sort_by { |_, v| v.presence.last_visible || 0 }.last
  most_recent ? Hash[[most_recent]] : {}
end

#add_client(mac) ⇒ void



27
28
29
# File 'lib/patronus_fati/data_models/access_point.rb', line 27

def add_client(mac)
  client_macs << mac unless client_macs.include?(mac)
end

#announce_changesvoid



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/patronus_fati/data_models/access_point.rb', line 31

def announce_changes
  return unless dirty? && valid? && worth_syncing?

  if active?
    status = new? ? :new : :changed

    PatronusFati.event_handler.event(
      :access_point,
      status,
      full_state,
      diagnostic_data
    )
  else
    PatronusFati.event_handler.event(
      :access_point, :offline, {
        'bssid' => local_attributes[:bssid],
        'uptime' => presence.visible_time
      },
      diagnostic_data
    )

    # We need to reset the first seen so we get fresh duration
    # information
    presence.first_seen = nil

    client_macs.each do |mac|
      DataModels::Client[mac].remove_access_point(local_attributes[:bssid])
      DataModels::Connection["#{local_attributes[:bssid]}^#{mac}"].link_lost = true
    end
  end

  mark_synced
end

#broadcasting_multiple?Boolean

Returns:

  • (Boolean)


65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/patronus_fati/data_models/access_point.rb', line 65

def broadcasting_multiple?
  return false unless ssids
  return false if active_ssids.count == 1

  presences = active_ssids.values.map(&:presence)
  # This check becomes very expensive at larger numbers, if we get too
  # high just short circuit and assume that yes there are simultaneous
  # SSIDs being transmitted. This is likely a sign of a malicious device.
  return true if presences.length >= 100

  current_presence_bits = presences.map { |p| p.current_presence.bits }
  return true if PatronusFati::BitHelper.largest_bit_overlap(current_presence_bits) >= SIMULTANEOUS_SSID_THRESHOLD

  last_presence_bits = presences.map { |p| p.last_presence.bits }
  return true if PatronusFati::BitHelper.largest_bit_overlap(last_presence_bits) >= SIMULTANEOUS_SSID_THRESHOLD

  false
end

#cleanup_ssidsvoid



84
85
86
87
88
89
90
91
92
# File 'lib/patronus_fati/data_models/access_point.rb', line 84

def cleanup_ssids
  return if ssids.nil? || ssids.select { |_, v| v.presence.dead? }.empty?

  # When an AP is offline we don't care about announcing that it's SSIDs
  # have expired, but we do want to remove them.
  set_sync_flag(:dirtyChildren) if active?

  ssids.reject! { |_, v| v.presence.dead? }
end

#diagnostic_datavoid



94
95
96
97
98
99
# File 'lib/patronus_fati/data_models/access_point.rb', line 94

def diagnostic_data
  dd = super
  dd.merge!(ssids: Hash[ssids.map { |k, s| [k, s.diagnostic_data] }]) if ssids
  dd[:last_dbm] = last_dbm if last_dbm
  dd
end

#full_statevoid



101
102
103
104
105
106
107
108
109
110
# File 'lib/patronus_fati/data_models/access_point.rb', line 101

def full_state
  state = local_attributes.merge({
    active: active?,
    broadcasting_multiple: broadcasting_multiple?,
    connected_clients: client_macs,
    vendor: vendor
  })
  state[:ssids] = active_ssids.values.map(&:full_state) if ssids
  state
end

#mark_syncedvoid



118
119
120
121
# File 'lib/patronus_fati/data_models/access_point.rb', line 118

def mark_synced
  super
  ssids.each { |_, v| v.mark_synced } if ssids
end

#remove_client(mac) ⇒ void



123
124
125
# File 'lib/patronus_fati/data_models/access_point.rb', line 123

def remove_client(mac)
  client_macs.delete(mac)
end

#track_ssid(ssid_data) ⇒ void



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/patronus_fati/data_models/access_point.rb', line 127

def track_ssid(ssid_data)
  self.ssids ||= {}

  ssid_key = ssid_data[:cloaked] ?
    Digest::SHA256.hexdigest(ssid_data[:crypt_set].join) :
    ssid_data[:essid]

  ssids[ssid_key] ||= DataModels::Ssid.new(ssid_data[:essid])

  ssid = ssids[ssid_key]
  ssid.presence.mark_visible
  ssid.update(ssid_data)

  set_sync_flag(:dirtyChildren) if ssid.dirty?
end

#update(attrs) ⇒ void



143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/patronus_fati/data_models/access_point.rb', line 143

def update(attrs)
  attrs.each do |k, v|
    next unless LOCAL_ATTRIBUTE_KEYS.include?(k)
    next if v.nil? || local_attributes[k] == v
    # Disregard channel band updates for a specific BSSID
    next if k == :channel && local_attributes[k] <= 13 && v > 13
    next if k == :channel && local_attributes[k] > 13 && v <= 13

    set_sync_flag(:dirtyAttributes)
    local_attributes[k] = v
  end
end

#valid?Boolean

Returns:

  • (Boolean)


156
157
158
159
# File 'lib/patronus_fati/data_models/access_point.rb', line 156

def valid?
  !([:bssid, :channel, :type].map { |k| local_attributes[k].nil? }.any?) &&
    local_attributes[:channel] != 0
end

#vendorvoid



161
162
163
164
165
# File 'lib/patronus_fati/data_models/access_point.rb', line 161

def vendor
  return unless local_attributes[:bssid]
  result = Louis.lookup(local_attributes[:bssid])
  result['long_vendor'] || result['short_vendor']
end

#worth_syncing?Boolean

This is a safety mechanism to check whether or not an access point is actually ‘present’. This is intended to assist in cutting out the access points that are just on the edge of being visible to our sensors.

Returns:

  • (Boolean)


170
171
172
173
# File 'lib/patronus_fati/data_models/access_point.rb', line 170

def worth_syncing?
  client_macs.any? || sync_flag?(:syncedOnline) ||
    (presence && presence.visible_time && presence.visible_time > INTERVAL_DURATION)
end