Class: LogStash::Filters::Phpipam

Inherits:
Base
  • Object
show all
Defined in:
lib/logstash/filters/phpipam.rb

Overview

A Logstash filter that looks up an IP-address, and returns results from phpIPAM

Instance Method Summary collapse

Instance Method Details

#closeObject



73
74
75
76
77
78
# File 'lib/logstash/filters/phpipam.rb', line 73

def close
  @logger.debug? && @logger.debug('Persisting databases...')

  # Persist the database to disk, when the pipeline ends
  @cs_ip.bgsave # Will persist all databases
end

#filter(event) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/logstash/filters/phpipam.rb', line 80

def filter(event)
  ip = event.get(@source)

  return if ip.nil?

  return unless valid_ip?(ip, event)

  # Get the data
  event_data = phpipam_data(ip)

  # Tag and return if no IP was found
  if event_data.nil?
    event.tag('_phpipam_ip_not_found')
    return
  end

  # Set the data to the target path
  event.set(@target, event_data)

  # filter_matched should go in the last line of our successful code
  filter_matched(event)
end

#nil_or_empty?(value) ⇒ bool

Checks whether the value is nil or empty

Parameters:

  • value:

    a value to check

Returns:

  • (bool)


183
184
185
# File 'lib/logstash/filters/phpipam.rb', line 183

def nil_or_empty?(value)
  value.nil? || value.empty?
end

#normalize_target(target) ⇒ string

make sure @target is in the format [field name] if defined, i.e. not empty and surrounded by brakets

Parameters:

  • target:

    the target to normalize

Returns:

  • (string)


107
108
109
110
# File 'lib/logstash/filters/phpipam.rb', line 107

def normalize_target(target)
  target = "[#{target}]" if target !~ %r{^\[[^\[\]]+\]$}
  target
end

#phpipam_data(ip) ⇒ hash/nil

Get phpIPAM data either from cache or phpIPAM. If data was found from phpIPAM, it will cache it for future needs. If the data wasn’t found in either cache or phpIPAM, nil is returned.

Parameters:

  • ip
    • IP-address to lookup

Returns:

  • (hash/nil)


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/logstash/filters/phpipam.rb', line 192

def phpipam_data(ip)
  # Base hash to format data in
  base = {
    'ip'       => {},
    'subnet'   => {},
    'vlan'     => {},
    'device'   => {},
    'location' => {},
  }

  # If 0 is returned, it has been cached as non-existent
  return nil if @cs_ip.get(ip) == '0'

  ## IP LOOKUP ##
  if @cs_ip.get(ip).nil?
    ip_data = send_rest_request('GET', "api/#{@app_id}/addresses/search/#{ip}/")

    # Return and cache 0 for this IP, if it wasn't found in phpIPAM
    if ip_data.nil?
      @cs_ip.set(ip, '0', ex: @cache_freshness)
      return nil
    end

    # IP information
    base['ip']['id']          = ip_data['id'].to_i
    base['ip']['address']     = ip_data['ip']
    base['ip']['description'] = ip_data['description'] unless nil_or_empty?(ip_data['description'])
    base['ip']['hostname']    = ip_data['hostname'] unless nil_or_empty?(ip_data['hostname'])
    base['ip']['mac']         = ip_data['mac'] unless nil_or_empty?(ip_data['mac'])
    base['ip']['note']        = ip_data['note'] unless nil_or_empty?(ip_data['note'])
    base['ip']['owner']       = ip_data['owner'] unless nil_or_empty?(ip_data['owner'])

    # Get all the ID's
    base['ip']['subnet_id']   = ip_data['subnetId'].to_i
    base['ip']['device_id']   = ip_data['deviceId'].to_i
    base['ip']['location_id'] = ip_data['location'].to_i

    @cs_ip.set(ip, base['ip'].to_json, ex: @cache_freshness)
  else
    base['ip'] = JSON.parse(@cs_ip.get(ip))
  end

  ## SUBNET LOOKUP ##
  subnet_id = base['ip']['subnet_id']

  # If 0 is returned, it doesn't exist. Only lookup if a nonzero value is returned
  if subnet_id.positive?
    if @cs_subnet.get(subnet_id).nil?
      subnet_data = send_rest_request('GET', "api/#{@app_id}/subnets/#{subnet_id}/")

      # Subnet data
      base['subnet']['id']         = subnet_id
      base['subnet']['section_id'] = subnet_data['sectionId'].to_i
      base['subnet']['bitmask']    = subnet_data['calculation']['Subnet bitmask'].to_i
      base['subnet']['wildcard']   = subnet_data['calculation']['Subnet wildcard']
      base['subnet']['netmask']    = subnet_data['calculation']['Subnet netmask']
      base['subnet']['network']    = subnet_data['calculation']['Network']

      # Get VLAN id and location _id
      base['subnet']['vlan_id']     = subnet_data['vlanId'].to_i
      base['subnet']['location_id'] = subnet_data['location'].to_i

      @cs_subnet.set(subnet_id, base['subnet'].to_json, ex: @cache_freshness)
    else
      base['subnet'] = JSON.parse(@cs_subnet.get(subnet_id))
    end
  end

  ## VLAN LOOKUP ##
  vlan_id = base['subnet']['vlan_id']

  # If 0 is returned, it doesn't exist. Only lookup if a nonzero value is returned
  if vlan_id.positive?
    if @cs_vlan.get(vlan_id).nil?
      vlan_data = send_rest_request('GET', "api/#{@app_id}/vlans/#{vlan_id}/")

      # VLAN data
      base['vlan']['id']          = vlan_id
      base['vlan']['domain_id']   = vlan_data['domainId'].to_i
      base['vlan']['number']      = vlan_data['number'].to_i unless nil_or_empty?(vlan_data['number'])
      base['vlan']['name']        = vlan_data['name'] unless nil_or_empty?(vlan_data['name'])
      base['vlan']['description'] = vlan_data['description'] unless nil_or_empty?(vlan_data['description'])

      @cs_vlan.set(vlan_id, base['vlan'].to_json, ex: @cache_freshness)
    else
      base['vlan'] = JSON.parse(@cs_vlan.get(vlan_id))
    end
  end

  ## DEVICE LOOKUP ##
  device_id = base['ip']['device_id']

  # If 0 is returned, it doesn't exist. Only lookup if a nonzero value is returned
  if device_id.positive?
    if @cs_device.get(device_id).nil?
      device_data = send_rest_request('GET', "api/#{@app_id}/tools/devices/#{device_id}/")
      type_id     = device_data['type']

      # Device type_name is another REST call
      if @cs_device_types.get(type_id).nil?
        type_name = send_rest_request('GET', "api/#{@app_id}/tools/device_types/#{type_id}/")['tname']

        @cs_device_types.set(type_id, type_name, ex: @cache_freshness)
      else
        type_name = @cs_device_types.get(type_id)
      end

      base['device']['id']          = device_id
      base['device']['name']        = device_data['hostname'] unless nil_or_empty?(device_data['hostname'])
      base['device']['description'] = device_data['description'] unless nil_or_empty?(device_data['description'])
      base['device']['type']        = type_name

      # Get device location
      base['device']['location_id'] = device_data['location'].to_i

      @cs_device.set(device_id, base['device'].to_json, ex: @cache_freshness)
    else
      base['device'] = JSON.parse(@cs_device.get(device_id))
    end
  end

  ## LOCATION LOOKUP ##
  # Get the first positive location_id from the list
  location_id = [base['ip']['location_id'], base['device']['location_id'], base['subnet']['location_id']].select { |num|
    !num.nil? && num.positive?
  }[0] || 0

  # If 0 is returned, it doesn't exist. Only lookup if a nonzero value is returned
  if location_id.positive?
    if @cs_location.get(location_id).nil?
      location_data = send_rest_request('GET', "api/#{@app_id}/tools/locations/#{location_id}/")

      # Location  data
      base['location']['id']          = location_id
      base['location']['address']     = location_data['address'] unless nil_or_empty?(location_data['address'])
      base['location']['name']        = location_data['name'] unless nil_or_empty?(location_data['name'])
      base['location']['description'] = location_data['description'] unless nil_or_empty?(location_data['description'])
      base['location']['location']    = { 'lat' => location_data['lat'].to_f, 'lon' => location_data['long'].to_f } unless nil_or_empty?(location_data['lat'])

      @cs_location.set(location_id, base['location'].to_json, ex: @cache_freshness)
    else
      base['location'] = JSON.parse(@cs_location.get(location_id))
    end
  end

  # Clean-up keys that aren't needed in the final Logstash output
  base['ip'].delete('subnet_id')
  base['ip'].delete('device_id')
  base['ip'].delete('location_id')
  base['subnet'].delete('vlan_id')
  base['subnet'].delete('location_id')
  base['device'].delete('location_id')
  base.delete_if { |_, val| val.empty? }

  # all your base are belong to us
  base

# Crash hard incase the connection to Redis stops
rescue Redis::CannotConnectError
  raise Redis::CannotConnectError, 'Lost connection to Redis!'
end

#registerObject

Raises:

  • (LogStash::ConfigurationError)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/logstash/filters/phpipam.rb', line 46

def register
  # Validate auth
  raise LogStash::ConfigurationError, 'Authentication was enabled, but no user/pass found' if @auth && (@username.empty? || @password.empty?)

  # Get a session token
  @token = send_rest_request('POST', "api/#{@app_id}/user/")['token'] if @auth

  # Normalize target
  @target = normalize_target(@target)

  @cache_freshness = @cache_freshness.to_i

  @cs_ip           = Redis.new(db: @cache_ip, id: 'logstash-filter-phpipam')
  @cs_subnet       = Redis.new(db: @cache_subnet, id: 'logstash-filter-phpipam')
  @cs_vlan         = Redis.new(db: @cache_vlan, id: 'logstash-filter-phpipam')
  @cs_device       = Redis.new(db: @cache_device, id: 'logstash-filter-phpipam')
  @cs_location     = Redis.new(db: @cache_location, id: 'logstash-filter-phpipam')
  @cs_device_types = Redis.new(db: @cache_device_types, id: 'logstash-filter-phpipam')

  # Validate Redis connection
  begin
    @cs_ip.ping
  rescue Redis::CannotConnectError
    raise Redis::CannotConnectError, 'Cannot connect to Redis!'
  end
end

#send_rest_request(method, url_path) ⇒ hash/nil

Sends a GET method REST request. Returns nil if no data/an error was found

Parameters:

  • method:

    which HTTP method to use (POST, GET)

  • url_path:

    path to connect to

Returns:

  • (hash/nil)

Raises:

  • (LogStash::ConfigurationError)


133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/logstash/filters/phpipam.rb', line 133

def send_rest_request(method, url_path)
  @logger.debug? && @logger.debug('Sending request', host: @host, path: url_path)

  url = URI("#{@host}/#{url_path}")

  http             = Net::HTTP.new(url.host, url.port)
  http.use_ssl     = url.scheme == 'https'
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE

  request = case method
            when 'POST' then Net::HTTP::Post.new(url)
            when 'GET' then Net::HTTP::Get.new(url)
            end

  request['accept']        = 'application/json'
  request['content-type']  = 'application/json'
  request['phpipam-token'] = @token unless @token.nil?
  request.basic_auth(@username, @password) if @token.nil? && @auth

  begin
    response = http.request(request)
  rescue StandardError
    raise LogStash::ConfigurationError, I18n.t(
      'logstash.runner.configuration.invalid_plugin_register',
      plugin: 'filter',
      type:   'phpipam',
      error:  'Could not connect to configured host',
    )
  end

  # Parse the body
  rsp = JSON.parse(response.body)

  # Raise an error if not a code 200 is returned
  raise LogStash::ConfigurationError, "#{rsp['code']}:#{rsp['message']}" if rsp['code'] != 200

  # Return nil if no data field is present, else return the data
  rsp = if rsp['data'].nil?
          nil
        else
          rsp['data'].is_a?(Array) ? rsp['data'][0] : rsp['data']
        end

  @logger.debug? && @logger.debug('Got response', body: response.body, data: rsp)
  rsp
end

#valid_ip?(ip, event) ⇒ bool

Validates an IP-address

Parameters:

  • ip:

    an IP-address

  • event:

    The Logstash event variable

Returns:

  • (bool)


116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/logstash/filters/phpipam.rb', line 116

def valid_ip?(ip, event)
  IPAddr.new(ip)

  @logger.debug? && @logger.debug('Valid IP', ip: ip)

  # Return true. Rescue would take over if a non-valid IP was parsed
  true
rescue StandardError
  @logger.debug? && @logger.debug('NOT a valid IP', ip: ip)
  event.tag('_phpipam_invalid_ip')
  false
end