Class: Inspec::Resources::LinuxPorts

Inherits:
PortsInfo
  • Object
show all
Defined in:
lib/resources/port.rb

Overview

extract port information from netstat

Constant Summary collapse

ALLOWED_PROTOCOLS =

rubocop:disable Metrics/ClassLength

%w{tcp tcp6 udp udp6}.freeze

Instance Attribute Summary

Attributes inherited from PortsInfo

#inspec

Instance Method Summary collapse

Methods inherited from PortsInfo

#initialize

Constructor Details

This class inherits a constructor from Inspec::Resources::PortsInfo

Instance Method Details

#infoObject



270
271
272
# File 'lib/resources/port.rb', line 270

def info
  ports_via_ss || ports_via_netstat
end

#parse_net_address(net_addr, protocol) ⇒ Object



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
353
354
355
356
357
# File 'lib/resources/port.rb', line 308

def parse_net_address(net_addr, protocol)
  if protocol.eql?('tcp6') || protocol.eql?('udp6')
    # prep for URI parsing, parse ip6 port
    ip6 = /^(\S+):(\d+)$/.match(net_addr)
    ip6addr = ip6[1]
    ip6addr = '::' if ip6addr =~ /^:::$/

    # v6 addresses need to end in a double-colon when using
    # shorthand notation. netstat ends with a single colon.
    # IPAddr will fail to properly parse an address unless it
    # uses a double-colon for short-hand notation.
    ip6addr += ':' if ip6addr =~ /\w:$/

    begin
      ip_parser = IPAddr.new(ip6addr)
    rescue IPAddr::InvalidAddressError
      # This IP is not parsable. There appears to be a bug in netstat
      # output that truncates link-local IP addresses:
      # example: udp6 0 0 fe80::42:acff:fe11::123 :::* 0 54550 3335/ntpd
      # actual link address: inet6 fe80::42:acff:fe11:5/64 scope link
      #
      # in this example, the "5" is truncated making the netstat output
      # an invalid IP address.
      return [nil, nil]
    end

    # Check to see if this is a IPv4 address in a tcp6/udp6 line.
    # If so, don't put brackets around the IP or URI won't know how
    # to properly handle it.
    # example: tcp6       0      0 127.0.0.1:8005          :::*                    LISTEN
    if ip_parser.ipv4?
      ip_addr = URI("addr://#{ip6addr}:#{ip6[2]}")
      host = ip_addr.host
    else
      ip_addr = URI("addr://[#{ip6addr}]:#{ip6[2]}")
      # strip []
      host = ip_addr.host[1..ip_addr.host.size-2]
    end
  else
    ip_addr = URI('addr://'+net_addr)
    host = ip_addr.host
  end

  port = ip_addr.port

  [host, port]
rescue URI::InvalidURIError => e
  warn "Could not parse #{net_addr}, #{e}"
  nil
end

#parse_netstat_line(line) ⇒ Object



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/resources/port.rb', line 359

def parse_netstat_line(line)
  # parse each line
  # 1 - Proto, 2 - Recv-Q, 3 - Send-Q, 4 - Local Address, 5 - Foreign Address, 6 - State, 7 - Inode, 8 - PID/Program name
  parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)?\s+(\S+)\s+(\S+)\s+(\S+)/.match(line)
  return {} if parsed.nil? || line.match(/^proto/i)

  # parse ip4 and ip6 addresses
  protocol = parsed[1].downcase

  # detect protocol if not provided
  protocol += '6' if parsed[4].count(':') > 1 && %w{tcp udp}.include?(protocol)

  # extract host and port information
  host, port = parse_net_address(parsed[4], protocol)
  return {} if host.nil?

  # extract PID
  process = parsed[9].split('/')
  pid = process[0]
  pid = pid.to_i if pid =~ /^\d+$/
  process = process[1]

  {
    'port'     => port,
    'address'  => host,
    'protocol' => protocol,
    'process'  => process,
    'pid'      => pid,
  }
end

#parse_ss_line(line) ⇒ Object



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/resources/port.rb', line 390

def parse_ss_line(line)
  parsed = line.split(/\s+/, 7)

  # ss only returns "tcp" and "udp" as the protocol. However, netstat would return
  # "tcp6" and "udp6" as necessary. In order to maintain backward compatibility, we
  # will manually modify the protocol value if the line we're parsing is an IPv6
  # entry.
  process_info = parsed[6]
  protocol = parsed[0]
  protocol += '6' if process_info.include?('v6only:1')
  return nil unless ALLOWED_PROTOCOLS.include?(protocol)

  # parse the Local Address:Port
  # examples:
  #   *:22
  #   :::22
  #   10.0.2.15:1234
  #   ::ffff:10.0.2.15:9300
  #   fe80::a00:27ff:fe32:ed09%enp0s3:9200
  parsed_net_address = parsed[4].match(/(\S+):(\*|\d+)$/)
  return nil if parsed_net_address.nil?
  host = parsed_net_address[1]
  port = parsed_net_address[2]
  return nil if host.nil? && port.nil?

  # For backward compatibility with the netstat output, ensure the
  # port is stored as an integer
  port = port.to_i

  # for those "v4-but-listed-in-v6" entries, strip off the
  # leading IPv6 value at the beginning
  # example: ::ffff:10.0.2.15:9200
  host.delete!('::ffff:') if host.start_with?('::ffff:')

  # if there's an interface name in the local address, which is common for
  # IPv6 listeners, strip that out too.
  # example: fe80::a00:27ff:fe32:ed09%enp0s3
  host = host.split('%').first

  # if host is "*", replace with "0.0.0.0" to maintain backward compatibility with
  # the netstat-provided data
  host = '0.0.0.0' if host == '*'

  # parse the process name from the processes information
  process_match = parsed[6].match(/users:\(\(\"(\S+)\"/)
  process = process_match.nil? ? nil : process_match[1]

  # parse the PID from the processes information
  pid_match = parsed[6].match(/pid=(\d+)/)
  pid = pid_match.nil? ? nil : pid_match[1].to_i

  {
    'port'     => port,
    'address'  => host,
    'protocol' => protocol,
    'process'  => process,
    'pid'      => pid,
  }
end

#ports_via_netstatObject



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/resources/port.rb', line 290

def ports_via_netstat
  return nil unless inspec.command('netstat').exist?

  cmd = inspec.command('netstat -tulpen')
  return nil unless cmd.exit_status.to_i.zero?

  ports = []
  # parse all lines
  cmd.stdout.each_line do |line|
    port_info = parse_netstat_line(line)

    # only push protocols we are interested in
    next unless %w{tcp tcp6 udp udp6}.include?(port_info['protocol'])
    ports.push(port_info)
  end
  ports
end

#ports_via_ssObject



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/resources/port.rb', line 274

def ports_via_ss
  return nil unless inspec.command('ss').exist?

  cmd = inspec.command('ss -tulpen')
  return nil unless cmd.exit_status.to_i.zero?

  ports = []

  cmd.stdout.each_line do |line|
    parsed_line = parse_ss_line(line)
    ports << parsed_line unless parsed_line.nil?
  end

  ports
end