class Inspec::Resources::LsofPorts

extracts udp and tcp ports from the lsof command

def info

def info
  ports = []
  # check that lsof is available, otherwise fail
  raise 'Please ensure `lsof` is available on the machine.' if !inspec.command(@lsof.to_s).exist?
  # -F p=pid, c=command, P=protocol name, t=type, n=internet addresses
  # see 'OUTPUT FOR OTHER PROGRAMS' in LSOF(8)
  lsof_cmd = inspec.command("#{@lsof} -nP -i -FpctPn")
  return nil if lsof_cmd.exit_status.to_i != 0
  # map to desired return struct
  lsof_parser(lsof_cmd).each do |process, port_ids|
    pid, cmd = process.split(':')
    port_ids.each do |port_str|
      # should not break on ipv6 addresses
      ipv, proto, port, host = port_str.split(':', 4)
      ports.push({ 'port'     => port.to_i,
                   'address'  => host,
                   'protocol' => ipv == 'ipv6' ? proto + '6' : proto,
                   'process'  => cmd,
                   'pid'      => pid.to_i })
    end
  end
  ports
end

def initialize(inspec, lsofpath = nil)

def initialize(inspec, lsofpath = nil)
  @lsof = lsofpath || 'lsof'
  super(inspec)
end

def lsof_parser(lsof_cmd)

rubocop:disable Metrics/AbcSize
rubocop:disable Metrics/CyclomaticComplexity
def lsof_parser(lsof_cmd)
  procs = {}
  # build this with formatted output (-F) from lsof
  # procs = {
  #   '123:sshd' => [
  #      'ipv4:tcp:22:127.0.0.1',
  #      'ipv6:tcp:22:::1',
  #      'ipv4:tcp:*',
  #      'ipv6:tcp:*',
  #   ],
  #   '456:ntpd' => [
  #      'ipv4:udp:123:*',
  #      'ipv6:udp:123:*',
  #   ]
  # }
  proc_id = port_id = nil
  lsof_cmd.stdout.each_line do |line|
    line.chomp!
    key = line.slice!(0)
    case key
    when 'p'
      proc_id = line
      port_id = nil
    when 'c'
      proc_id += ':' + line
    when 't'
      port_id = line.downcase
    when 'P'
      port_id += ':' + line.downcase
    when 'n'
      src, dst = line.split('->')
      # skip active comm streams
      next if dst
      host, port = /^(\S+):(\d+|\*)$/.match(src)[1, 2]
      # skip channels from port 0 - what does this mean?
      next if port == '*'
      # create new array stub if !exist?
      procs[proc_id] = [] unless procs.key?(proc_id)
      # change address '*' to zero
      host = port_id =~ /^ipv6:/ ? '[::]' : '0.0.0.0' if host == '*'
      # entrust URI to scrub the host and port
      begin
        uri = URI("addr://#{host}:#{port}")
        uri.host && uri.port
      rescue => e
        warn "could not parse URI 'addr://#{host}:#{port}' - #{e}"
        next
      end
      # e.g. 'ipv4:tcp:22:127.0.0.1'
      #                             strip ipv6 squares for inspec
      port_id += ':' + port + ':' + host.gsub(/^\[|\]$/, '')
      # lsof will give us another port unless it's done
      procs[proc_id] << port_id
    end
  end
  procs
end