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