class Inspec::Resources::LinuxPorts

extract port information from netstat

Constants

ALLOWED_PROTOCOLS

Public Instance Methods

info() click to toggle source
# File lib/inspec/resources/port.rb, line 391
def info
  ports_via_ss || ports_via_netstat
end
parse_net_address(net_addr, protocol) click to toggle source
# File lib/inspec/resources/port.rb, line 435
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) click to toggle source
# File lib/inspec/resources/port.rb, line 486
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 - User, 8 - Inode, 9 - PID/Program name
  # * UDP lines have an empty State column and the Busybox variant lacks
  # the User and Inode columns.
  reg =  /^(?<proto>\S+)\s+(\S+)\s+(\S+)\s+(?<local_addr>\S+)\s+(?<foreign_addr>\S+)\s+(\S+)?\s+((\S+)\s+(\S+)\s+)?(?<pid_prog>\S+)/
  parsed = reg.match(line)

  return {} if parsed.nil? || line.match(/^proto/i)

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

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

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

  # extract PID
  process = parsed[:pid_prog].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) click to toggle source
# File lib/inspec/resources/port.rb, line 547
def parse_ss_line(line)
  # parsed = line.split(/\s+/, 7)
  parsed = tokenize_ss_line(line)

  # 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[:process_info]
  protocol = parsed[:netid]
  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[:local_addr].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:")

  # To remove brackets that might surround the IPv6 address
  # example: [::] and [fe80::dc11:b9b6:514b:134]%eth0:123
  host = host.tr("[]", "")

  # 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 == "*"

  # in case process list parsing is not successfull
  process = nil
  pid = nil

  # parse process and pid from the process list
  #
  # remove the "users:((" and  "))" parts
  # input: users:((\"nginx\",pid=583,fd=8),(\"nginx\",pid=582,fd=8),(\"nginx\",pid=580,fd=8),(\"nginx\",pid=579,fd=8))
  # res: \"nginx\",pid=583,fd=8),(\"nginx\",pid=582,fd=8),(\"nginx\",pid=580,fd=8),(\"nginx\",pid=579,fd=8
  process_list_match = parsed[:process_info].match(/users:\(\((.+)\)\)/)
  if process_list_match
    # list entires are seperated by "," the braces can also be removed
    # input: \"nginx\",pid=583,fd=8),(\"nginx\",pid=582,fd=8),(\"nginx\",pid=580,fd=8),(\"nginx\",pid=579,fd=8
    # res: ["\"nginx\",pid=583,fd=8", "\"nginx\",pid=582,fd=8", "\"nginx\",pid=580,fd=8", "\"nginx\",pid=579,fd=8"]
    process_list = process_list_match[1].split("),(")
    # To stay backwards compatible with netstat we need to select
    # the last element in the resulting array.
    # res: "\"nginx\",pid=579,fd=8"

    # parse the process name from the process list
    process_match = process_list.last.match(/^\"(\S+)\"/)
    process = process_match.nil? ? nil : process_match[1]

    # parse the PID from the process list
    pid_match = process_list.last.match(/pid=(\d+)/)
    pid = pid_match.nil? ? nil : pid_match[1].to_i
  end

  {
    "port" => port,
    "address" => host,
    "protocol" => protocol,
    "process" => process,
    "pid" => pid,
  }
end
ports_via_netstat() click to toggle source
# File lib/inspec/resources/port.rb, line 416
def ports_via_netstat
  return nil unless inspec.command("netstat").exist?

  cmd = inspec.command("netstat -tulpen")
  return nil unless cmd.exit_status.to_i == 0

  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_ss() click to toggle source
# File lib/inspec/resources/port.rb, line 395
def ports_via_ss
  return nil unless inspec.command("ss").exist?

  if @port.nil?
    cmd = inspec.command("ss -tulpen")
  else
    cmd = inspec.command("ss -tulpen '( dport = #{@port} or sport = #{@port} )'")
  end

  return nil unless cmd.exit_status.to_i == 0

  ports = []

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

  ports
end
tokenize_ss_line(line) click to toggle source
# File lib/inspec/resources/port.rb, line 521
def tokenize_ss_line(line)
  # iproute-2.6.32-54.el6 output:
  # Netid State      Recv-Q Send-Q  Local Address:Port Peer Address:Port
  # udp   UNCONN     0      0       *:111              *:*                 users:(("rpcbind",1123,6)) ino=8680 sk=ffff8801390cf7c0
  # tcp   LISTEN     0      128     *:22               *:*                 users:(("sshd",3965,3)) ino:11604 sk:ffff88013a3b5800
  #
  # iproute-2.6.32-20.el6 output:
  # Netid            Recv-Q Send-Q  Local Address:Port Peer Address:Port
  # udp              0      0       *:111              *:*                 users:(("rpcbind",1123,6)) ino=8680 sk=ffff8801390cf7c0
  # tcp              0      128     *:22               *:*                 users:(("sshd",3965,3)) ino:11604 sk:ffff88013a3b5800
  tokens = line.split(/\s+/, 7)
  if tokens[1] =~ /^\d+$/ # iproute-2.6.32-20
    {
      netid: tokens[0],
      local_addr: tokens[3],
      process_info: tokens[5],
    }
  else # iproute-2.6.32-54
    {
      netid: tokens[0],
      local_addr: tokens[4],
      process_info: tokens[6],
    }
  end
end